初始化 PostgreSQL 租户控制器项目
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
# This file configures golangci-lint with module plugins.
|
||||||
|
# When you run 'make lint', it will automatically build a custom golangci-lint binary
|
||||||
|
# with all the plugins listed below.
|
||||||
|
#
|
||||||
|
# See: https://golangci-lint.run/plugins/module-plugins/
|
||||||
|
version: v2.12.2
|
||||||
|
plugins:
|
||||||
|
# logcheck validates structured logging calls and parameters (e.g., balanced key-value pairs)
|
||||||
|
- module: "sigs.k8s.io/logtools"
|
||||||
|
import: "sigs.k8s.io/logtools/logcheck/gclplugin"
|
||||||
|
version: latest
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "Kubebuilder DevContainer",
|
||||||
|
"image": "golang:1.26",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||||
|
"moby": false,
|
||||||
|
"dockerDefaultAddressPool": "base=172.30.0.0/16,size=24"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/git:1": {},
|
||||||
|
"ghcr.io/devcontainers/features/common-utils:2": {
|
||||||
|
"upgradePackages": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"runArgs": ["--privileged", "--init"],
|
||||||
|
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"settings": {
|
||||||
|
"terminal.integrated.shell.linux": "/bin/bash"
|
||||||
|
},
|
||||||
|
"extensions": [
|
||||||
|
"ms-kubernetes-tools.vscode-kubernetes-tools",
|
||||||
|
"ms-azuretools.vscode-docker"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"remoteEnv": {
|
||||||
|
"GO111MODULE": "on"
|
||||||
|
},
|
||||||
|
|
||||||
|
"onCreateCommand": "bash .devcontainer/post-install.sh"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "===================================="
|
||||||
|
echo "Kubebuilder DevContainer Setup"
|
||||||
|
echo "===================================="
|
||||||
|
|
||||||
|
# Verify running as root (required for installing to /usr/local/bin and /etc)
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "ERROR: This script must be run as root"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Detecting system architecture..."
|
||||||
|
# Detect architecture using uname
|
||||||
|
MACHINE=$(uname -m)
|
||||||
|
case "${MACHINE}" in
|
||||||
|
x86_64)
|
||||||
|
ARCH="amd64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
ARCH="arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "WARNING: Unsupported architecture ${MACHINE}, defaulting to amd64"
|
||||||
|
ARCH="amd64"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo "Architecture: ${ARCH}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "------------------------------------"
|
||||||
|
echo "Setting up bash completion..."
|
||||||
|
echo "------------------------------------"
|
||||||
|
|
||||||
|
BASH_COMPLETIONS_DIR="/usr/share/bash-completion/completions"
|
||||||
|
|
||||||
|
# Enable bash-completion in root's .bashrc (devcontainer runs as root)
|
||||||
|
if ! grep -q "source /usr/share/bash-completion/bash_completion" ~/.bashrc 2>/dev/null; then
|
||||||
|
echo 'source /usr/share/bash-completion/bash_completion' >> ~/.bashrc
|
||||||
|
echo "Added bash-completion to .bashrc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "------------------------------------"
|
||||||
|
echo "Installing development tools..."
|
||||||
|
echo "------------------------------------"
|
||||||
|
|
||||||
|
# Install kind
|
||||||
|
if ! command -v kind &> /dev/null; then
|
||||||
|
echo "Installing kind..."
|
||||||
|
curl -Lo /usr/local/bin/kind "https://kind.sigs.k8s.io/dl/latest/kind-linux-${ARCH}"
|
||||||
|
chmod +x /usr/local/bin/kind
|
||||||
|
echo "kind installed successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate kind bash completion
|
||||||
|
if command -v kind &> /dev/null; then
|
||||||
|
if kind completion bash > "${BASH_COMPLETIONS_DIR}/kind" 2>/dev/null; then
|
||||||
|
echo "kind completion installed"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to generate kind completion"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install kubebuilder
|
||||||
|
if ! command -v kubebuilder &> /dev/null; then
|
||||||
|
echo "Installing kubebuilder..."
|
||||||
|
curl -Lo /usr/local/bin/kubebuilder "https://go.kubebuilder.io/dl/latest/linux/${ARCH}"
|
||||||
|
chmod +x /usr/local/bin/kubebuilder
|
||||||
|
echo "kubebuilder installed successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate kubebuilder bash completion
|
||||||
|
if command -v kubebuilder &> /dev/null; then
|
||||||
|
if kubebuilder completion bash > "${BASH_COMPLETIONS_DIR}/kubebuilder" 2>/dev/null; then
|
||||||
|
echo "kubebuilder completion installed"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to generate kubebuilder completion"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install kubectl
|
||||||
|
if ! command -v kubectl &> /dev/null; then
|
||||||
|
echo "Installing kubectl..."
|
||||||
|
KUBECTL_VERSION=$(curl -Ls https://dl.k8s.io/release/stable.txt)
|
||||||
|
curl -Lo /usr/local/bin/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl"
|
||||||
|
chmod +x /usr/local/bin/kubectl
|
||||||
|
echo "kubectl installed successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate kubectl bash completion
|
||||||
|
if command -v kubectl &> /dev/null; then
|
||||||
|
if kubectl completion bash > "${BASH_COMPLETIONS_DIR}/kubectl" 2>/dev/null; then
|
||||||
|
echo "kubectl completion installed"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to generate kubectl completion"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate Docker bash completion
|
||||||
|
if command -v docker &> /dev/null; then
|
||||||
|
if docker completion bash > "${BASH_COMPLETIONS_DIR}/docker" 2>/dev/null; then
|
||||||
|
echo "docker completion installed"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to generate docker completion"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "------------------------------------"
|
||||||
|
echo "Configuring Docker environment..."
|
||||||
|
echo "------------------------------------"
|
||||||
|
|
||||||
|
# Wait for Docker to be ready
|
||||||
|
echo "Waiting for Docker to be ready..."
|
||||||
|
for i in {1..30}; do
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
echo "Docker is ready"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$i" -eq 30 ]; then
|
||||||
|
echo "WARNING: Docker not ready after 30s"
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create kind network (ignore if already exists)
|
||||||
|
if ! docker network inspect kind >/dev/null 2>&1; then
|
||||||
|
if docker network create kind >/dev/null 2>&1; then
|
||||||
|
echo "Created kind network"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to create kind network (may already exist)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "------------------------------------"
|
||||||
|
echo "Verifying installations..."
|
||||||
|
echo "------------------------------------"
|
||||||
|
kind version
|
||||||
|
kubebuilder version
|
||||||
|
kubectl version --client
|
||||||
|
docker --version
|
||||||
|
go version
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "===================================="
|
||||||
|
echo "DevContainer ready!"
|
||||||
|
echo "===================================="
|
||||||
|
echo "All development tools installed successfully."
|
||||||
|
echo "You can now start building Kubernetes operators."
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||||
|
# Ignore everything by default and re-include only needed files
|
||||||
|
**
|
||||||
|
|
||||||
|
# Re-include Go source files (but not *_test.go)
|
||||||
|
!**/*.go
|
||||||
|
**/*_test.go
|
||||||
|
|
||||||
|
# Re-include Go module files
|
||||||
|
!go.mod
|
||||||
|
!go.sum
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
name: Run on Ubuntu
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone the code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Check linter configuration
|
||||||
|
run: make lint-config
|
||||||
|
- name: Run linter
|
||||||
|
run: make lint
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-e2e:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
name: Run on Ubuntu
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone the code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Install the latest version of kind
|
||||||
|
run: |
|
||||||
|
curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH)
|
||||||
|
chmod +x ./kind
|
||||||
|
sudo mv ./kind /usr/local/bin/kind
|
||||||
|
|
||||||
|
- name: Verify kind installation
|
||||||
|
run: kind version
|
||||||
|
|
||||||
|
- name: Running Test e2e
|
||||||
|
run: |
|
||||||
|
go mod tidy
|
||||||
|
make test-e2e
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
name: Run on Ubuntu
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone the code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Running Tests
|
||||||
|
run: |
|
||||||
|
go mod tidy
|
||||||
|
make test
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
bin/*
|
||||||
|
Dockerfile.cross
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# Kubernetes Generated files - skip generated files, except for vendored files
|
||||||
|
!vendor/**/zz_generated.*
|
||||||
|
|
||||||
|
# editor and IDE paraphernalia
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Kubeconfig might contain secrets
|
||||||
|
*.kubeconfig
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
allow-parallel-runners: true
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- copyloopvar
|
||||||
|
- depguard
|
||||||
|
- dupl
|
||||||
|
- errcheck
|
||||||
|
- ginkgolinter
|
||||||
|
- goconst
|
||||||
|
- gocyclo
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- modernize
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- prealloc
|
||||||
|
- revive
|
||||||
|
- staticcheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
- logcheck
|
||||||
|
settings:
|
||||||
|
custom:
|
||||||
|
logcheck:
|
||||||
|
type: "module"
|
||||||
|
description: Checks Go logging calls for Kubernetes logging conventions.
|
||||||
|
depguard:
|
||||||
|
rules:
|
||||||
|
forbid-sort-pkg:
|
||||||
|
deny:
|
||||||
|
- pkg: sort
|
||||||
|
desc: Should be replaced with slices package
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: comment-spacings
|
||||||
|
- name: import-shadowing
|
||||||
|
modernize:
|
||||||
|
disable:
|
||||||
|
- omitzero
|
||||||
|
- newexpr
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
rules:
|
||||||
|
- linters:
|
||||||
|
- lll
|
||||||
|
path: api/*
|
||||||
|
- linters:
|
||||||
|
- dupl
|
||||||
|
- lll
|
||||||
|
path: internal/*
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
# postgresql-tenant-operator - AI Agent Guide
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
**Single-group layout (default):**
|
||||||
|
```
|
||||||
|
cmd/main.go Manager entry (registers controllers/webhooks)
|
||||||
|
api/<version>/*_types.go CRD schemas (+kubebuilder markers)
|
||||||
|
api/<version>/zz_generated.* Auto-generated (DO NOT EDIT)
|
||||||
|
internal/controller/* Reconciliation logic
|
||||||
|
internal/webhook/* Validation/defaulting (if present)
|
||||||
|
config/crd/bases/* Generated CRDs (DO NOT EDIT)
|
||||||
|
config/rbac/role.yaml Generated RBAC (DO NOT EDIT)
|
||||||
|
config/samples/* Example CRs (edit these)
|
||||||
|
Makefile Build/test/deploy commands
|
||||||
|
PROJECT Kubebuilder metadata Auto-generated (DO NOT EDIT)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multi-group layout** (for projects with multiple API groups):
|
||||||
|
```
|
||||||
|
api/<group>/<version>/*_types.go CRD schemas by group
|
||||||
|
internal/controller/<group>/* Controllers by group
|
||||||
|
internal/webhook/<group>/<version>/* Webhooks by group and version (if present)
|
||||||
|
```
|
||||||
|
|
||||||
|
Multi-group layout organizes APIs by group name (e.g., `batch`, `apps`). Check the `PROJECT` file for `multigroup: true`.
|
||||||
|
|
||||||
|
**To convert to multi-group layout:**
|
||||||
|
1. Run: `kubebuilder edit --multigroup=true`
|
||||||
|
2. Move APIs: `mkdir -p api/<group> && mv api/<version> api/<group>/`
|
||||||
|
3. Move controllers: `mkdir -p internal/controller/<group> && mv internal/controller/*.go internal/controller/<group>/`
|
||||||
|
4. Move webhooks (if present): `mkdir -p internal/webhook/<group> && mv internal/webhook/<version> internal/webhook/<group>/`
|
||||||
|
5. Update import paths in all files
|
||||||
|
6. Fix `path` in `PROJECT` file for each resource
|
||||||
|
7. Update test suite CRD paths (add one more `..` to relative paths)
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
### Never Edit These (Auto-Generated)
|
||||||
|
- `config/crd/bases/*.yaml` - from `make manifests`
|
||||||
|
- `config/rbac/role.yaml` - from `make manifests`
|
||||||
|
- `config/webhook/manifests.yaml` - from `make manifests`
|
||||||
|
- `**/zz_generated.*.go` - from `make generate`
|
||||||
|
- `PROJECT` - from `kubebuilder [OPTIONS]`
|
||||||
|
|
||||||
|
### Never Remove Scaffold Markers
|
||||||
|
Do NOT delete `// +kubebuilder:scaffold:*` comments. CLI injects code at these markers.
|
||||||
|
|
||||||
|
### Keep Project Structure
|
||||||
|
Do not move files around. The CLI expects files in specific locations.
|
||||||
|
|
||||||
|
### Always Use CLI Commands
|
||||||
|
Always use `kubebuilder create api` and `kubebuilder create webhook` to scaffold. Do NOT create files manually.
|
||||||
|
|
||||||
|
### E2E Tests Require an Isolated Kind Cluster
|
||||||
|
The e2e tests are designed to validate the solution in an isolated environment (similar to GitHub Actions CI).
|
||||||
|
Ensure you run them against a dedicated [Kind](https://kind.sigs.k8s.io/) cluster (not your “real” dev/prod cluster).
|
||||||
|
|
||||||
|
## After Making Changes
|
||||||
|
|
||||||
|
**After editing `*_types.go` or markers:**
|
||||||
|
```
|
||||||
|
make manifests # Regenerate CRDs/RBAC from markers
|
||||||
|
make generate # Regenerate DeepCopy methods
|
||||||
|
```
|
||||||
|
|
||||||
|
**After editing `*.go` files:**
|
||||||
|
```
|
||||||
|
make lint-fix # Auto-fix code style
|
||||||
|
make test # Run unit tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Commands Cheat Sheet
|
||||||
|
|
||||||
|
### Create API (your own types)
|
||||||
|
```bash
|
||||||
|
kubebuilder create api --group <group> --version <version> --kind <Kind>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy Image Plugin (scaffold to deploy/manage ANY container image)
|
||||||
|
|
||||||
|
Generate a controller that deploys and manages a container image (nginx, redis, memcached, your app, etc.):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example: deploying memcached
|
||||||
|
kubebuilder create api --group example.com --version v1alpha1 --kind Memcached \
|
||||||
|
--image=memcached:alpine \
|
||||||
|
--plugins=deploy-image.go.kubebuilder.io/v1-alpha
|
||||||
|
```
|
||||||
|
|
||||||
|
Scaffolds good-practice code: reconciliation logic, status conditions, finalizers, RBAC. Use as a reference implementation.
|
||||||
|
|
||||||
|
|
||||||
|
### Create Webhooks
|
||||||
|
```bash
|
||||||
|
# Validation + defaulting
|
||||||
|
kubebuilder create webhook --group <group> --version <version> --kind <Kind> \
|
||||||
|
--defaulting --programmatic-validation
|
||||||
|
|
||||||
|
# Conversion webhook (for multi-version APIs)
|
||||||
|
kubebuilder create webhook --group <group> --version v1 --kind <Kind> \
|
||||||
|
--conversion --spoke v2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controller for Core Kubernetes Types
|
||||||
|
```bash
|
||||||
|
# Watch Pods
|
||||||
|
kubebuilder create api --group core --version v1 --kind Pod \
|
||||||
|
--controller=true --resource=false
|
||||||
|
|
||||||
|
# Watch Deployments
|
||||||
|
kubebuilder create api --group apps --version v1 --kind Deployment \
|
||||||
|
--controller=true --resource=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controller for External Types (e.g., from other operators)
|
||||||
|
|
||||||
|
Watch resources from external APIs (cert-manager, Argo CD, Istio, etc.):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example: watching cert-manager Certificate resources
|
||||||
|
kubebuilder create api \
|
||||||
|
--group cert-manager --version v1 --kind Certificate \
|
||||||
|
--controller=true --resource=false \
|
||||||
|
--external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
|
||||||
|
--external-api-domain=io \
|
||||||
|
--external-api-module=github.com/cert-manager/cert-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Use `--external-api-module=<module>@<version>` only if you need a specific version. Otherwise, omit `@<version>` to use what's in go.mod.
|
||||||
|
|
||||||
|
### Webhook for External Types
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example: validating external resources
|
||||||
|
kubebuilder create webhook \
|
||||||
|
--group cert-manager --version v1 --kind Issuer \
|
||||||
|
--defaulting \
|
||||||
|
--external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
|
||||||
|
--external-api-domain=io \
|
||||||
|
--external-api-module=github.com/cert-manager/cert-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing & Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test # Run unit tests (uses envtest: real K8s API + etcd)
|
||||||
|
make run # Run locally (uses current kubeconfig context)
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests use **Ginkgo + Gomega** (BDD style). Check `suite_test.go` for setup.
|
||||||
|
|
||||||
|
## Deployment Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Regenerate manifests
|
||||||
|
make manifests generate
|
||||||
|
|
||||||
|
# 2. Build & deploy
|
||||||
|
export IMG=<registry>/<project>:tag
|
||||||
|
make docker-build docker-push IMG=$IMG # Or: kind load docker-image $IMG --name <cluster>
|
||||||
|
make deploy IMG=$IMG
|
||||||
|
|
||||||
|
# 3. Test
|
||||||
|
kubectl apply -k config/samples/
|
||||||
|
|
||||||
|
# 4. Debug
|
||||||
|
kubectl logs -n <project>-system deployment/<project>-controller-manager -c manager -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Design
|
||||||
|
|
||||||
|
**Key markers for** `api/<version>/*_types.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
// +kubebuilder:subresource:status
|
||||||
|
// +kubebuilder:resource:scope=Namespaced
|
||||||
|
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=".status.conditions[?(@.type=='Ready')].status"
|
||||||
|
|
||||||
|
// On fields:
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
// +kubebuilder:validation:Minimum=1
|
||||||
|
// +kubebuilder:validation:MaxLength=100
|
||||||
|
// +kubebuilder:validation:Pattern="^[a-z]+$"
|
||||||
|
// +kubebuilder:default="value"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Use** `metav1.Condition` for status (not custom string fields)
|
||||||
|
- **Use predefined types**: `metav1.Time` instead of `string` for dates
|
||||||
|
- **Follow K8s API conventions**: Standard field names (`spec`, `status`, `metadata`)
|
||||||
|
|
||||||
|
### Controller Design
|
||||||
|
|
||||||
|
**RBAC markers in** `internal/controller/*_controller.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/finalizers,verbs=update
|
||||||
|
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
|
||||||
|
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation rules:**
|
||||||
|
- **Idempotent reconciliation**: Safe to run multiple times
|
||||||
|
- **Re-fetch before updates**: `r.Get(ctx, req.NamespacedName, obj)` before `r.Update` to avoid conflicts
|
||||||
|
- **Structured logging**: `log := log.FromContext(ctx); log.Info("msg", "key", val)`
|
||||||
|
- **Owner references**: Enable automatic garbage collection (`SetControllerReference`)
|
||||||
|
- **Watch secondary resources**: Use `.Owns()` or `.Watches()`, not just `RequeueAfter`
|
||||||
|
- **Finalizers**: Clean up external resources (buckets, VMs, DNS entries)
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
**Follow Kubernetes logging message style guidelines:**
|
||||||
|
|
||||||
|
- Start from a capital letter
|
||||||
|
- Do not end the message with a period
|
||||||
|
- Active voice: subject present (`"Deployment could not create Pod"`) or omitted (`"Could not create Pod"`)
|
||||||
|
- Past tense: `"Could not delete Pod"` not `"Cannot delete Pod"`
|
||||||
|
- Specify object type: `"Deleted Pod"` not `"Deleted"`
|
||||||
|
- Balanced key-value pairs
|
||||||
|
|
||||||
|
```go
|
||||||
|
log.Info("Starting reconciliation")
|
||||||
|
log.Info("Created Deployment", "name", deploy.Name)
|
||||||
|
log.Error(err, "Failed to create Pod", "name", name)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reference:** https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines
|
||||||
|
|
||||||
|
### Webhooks
|
||||||
|
- **Create all types together**: `--defaulting --programmatic-validation --conversion`
|
||||||
|
- **When`--force`is used**: Backup custom logic first, then restore after scaffolding
|
||||||
|
- **For multi-version APIs**: Use hub-and-spoke pattern (`--conversion --spoke v2`)
|
||||||
|
- Hub version: Usually oldest stable version (v1)
|
||||||
|
- Spoke versions: Newer versions that convert to/from hub (v2, v3)
|
||||||
|
- Example: `--group crew --version v1 --kind Captain --conversion --spoke v2` (v1 is hub, v2 is spoke)
|
||||||
|
|
||||||
|
### Learning from Examples
|
||||||
|
|
||||||
|
The **deploy-image plugin** scaffolds a complete controller following good practices. Use it as a reference implementation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubebuilder create api --group example --version v1alpha1 --kind MyApp \
|
||||||
|
--image=<your-image> --plugins=deploy-image.go.kubebuilder.io/v1-alpha
|
||||||
|
```
|
||||||
|
|
||||||
|
Generated code includes: status conditions (`metav1.Condition`), finalizers, owner references, events, idempotent reconciliation.
|
||||||
|
|
||||||
|
## Distribution Options
|
||||||
|
|
||||||
|
### Option 1: YAML Bundle (Kustomize)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate dist/install.yaml from Kustomize manifests
|
||||||
|
make build-installer IMG=<registry>/<project>:tag
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key points:**
|
||||||
|
- The `dist/install.yaml` is generated from Kustomize manifests (CRDs, RBAC, Deployment)
|
||||||
|
- Commit this file to your repository for easy distribution
|
||||||
|
- Users only need `kubectl` to install (no additional tools required)
|
||||||
|
|
||||||
|
**Example:** Users install with a single command:
|
||||||
|
```bash
|
||||||
|
kubectl apply -f https://raw.githubusercontent.com/<org>/<repo>/<tag>/dist/install.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Helm Chart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubebuilder edit --plugins=helm/v2-alpha # Generates dist/chart/ (default)
|
||||||
|
kubebuilder edit --plugins=helm/v2-alpha --output-dir=charts # Generates charts/chart/
|
||||||
|
```
|
||||||
|
|
||||||
|
**For development:**
|
||||||
|
```bash
|
||||||
|
make helm-deploy IMG=<registry>/<project>:<tag> # Deploy manager via Helm
|
||||||
|
make helm-deploy IMG=$IMG HELM_EXTRA_ARGS="--set ..." # Deploy with custom values
|
||||||
|
make helm-status # Show release status
|
||||||
|
make helm-uninstall # Remove release
|
||||||
|
make helm-history # View release history
|
||||||
|
make helm-rollback # Rollback to previous version
|
||||||
|
```
|
||||||
|
|
||||||
|
**For end users/production:**
|
||||||
|
```bash
|
||||||
|
helm install my-release ./<output-dir>/chart/ --namespace <ns> --create-namespace
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** If you add webhooks or modify manifests after initial chart generation:
|
||||||
|
1. Backup any customizations in `<output-dir>/chart/values.yaml` and `<output-dir>/chart/manager/manager.yaml`
|
||||||
|
2. Re-run: `kubebuilder edit --plugins=helm/v2-alpha --force` (use same `--output-dir` if customized)
|
||||||
|
3. Manually restore your custom values from the backup
|
||||||
|
|
||||||
|
### Publish Container Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export IMG=<registry>/<project>:<version>
|
||||||
|
make docker-build docker-push IMG=$IMG
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
### Essential Reading
|
||||||
|
- **Kubebuilder Book**: https://book.kubebuilder.io (comprehensive guide)
|
||||||
|
- **controller-runtime FAQ**: https://github.com/kubernetes-sigs/controller-runtime/blob/main/FAQ.md (common patterns and questions)
|
||||||
|
- **Good Practices**: https://book.kubebuilder.io/reference/good-practices.html (why reconciliation is idempotent, status conditions, etc.)
|
||||||
|
- **Logging Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines (message style, verbosity levels)
|
||||||
|
|
||||||
|
### API Design & Implementation
|
||||||
|
- **API Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
|
||||||
|
- **Operator Pattern**: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
|
||||||
|
- **Markers Reference**: https://book.kubebuilder.io/reference/markers.html
|
||||||
|
|
||||||
|
### Tools & Libraries
|
||||||
|
- **controller-runtime**: https://github.com/kubernetes-sigs/controller-runtime
|
||||||
|
- **controller-tools**: https://github.com/kubernetes-sigs/controller-tools
|
||||||
|
- **Kubebuilder Repo**: https://github.com/kubernetes-sigs/kubebuilder
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
# Build the manager binary
|
||||||
|
FROM golang:1.26 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 (relies on .dockerignore to filter)
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build
|
||||||
|
# the GOARCH has no default value to allow the binary to 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"]
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
# Image URL to use all building/pushing image targets
|
||||||
|
IMG ?= controller:latest
|
||||||
|
# YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header.
|
||||||
|
YEAR ?= $(shell date +%Y)
|
||||||
|
|
||||||
|
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||||
|
ifeq (,$(shell go env GOBIN))
|
||||||
|
GOBIN=$(shell go env GOPATH)/bin
|
||||||
|
else
|
||||||
|
GOBIN=$(shell go env GOBIN)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# CONTAINER_TOOL defines the container tool to be used for building images.
|
||||||
|
# Be aware that the target commands are only tested with Docker which is
|
||||||
|
# scaffolded by default. However, you might want to replace it to use other
|
||||||
|
# tools. (i.e. podman)
|
||||||
|
CONTAINER_TOOL ?= docker
|
||||||
|
|
||||||
|
# Setting SHELL to bash allows bash commands to be executed by recipes.
|
||||||
|
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
|
||||||
|
SHELL = /usr/bin/env bash -o pipefail
|
||||||
|
.SHELLFLAGS = -ec
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: build
|
||||||
|
|
||||||
|
##@ General
|
||||||
|
|
||||||
|
# The help target prints out all targets with their descriptions organized
|
||||||
|
# beneath their categories. The categories are represented by '##@' and the
|
||||||
|
# target descriptions by '##'. The awk command is responsible for reading the
|
||||||
|
# entire set of makefiles included in this invocation, looking for lines of the
|
||||||
|
# file as xyz: ## something, and then pretty-format the target and help. Then,
|
||||||
|
# if there's a line with ##@ something, that gets pretty-printed as a category.
|
||||||
|
# More info on the usage of ANSI control characters for terminal formatting:
|
||||||
|
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
|
||||||
|
# More info on the awk command:
|
||||||
|
# http://linuxcommand.org/lc3_adv_awk.php
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Display this help.
|
||||||
|
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\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",year=$(YEAR) 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 setup-envtest ## Run tests.
|
||||||
|
KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out
|
||||||
|
|
||||||
|
# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
|
||||||
|
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
|
||||||
|
# kubectl kuberc is disabled by default for test isolation; enable with:
|
||||||
|
# - KUBECTL_KUBERC=true
|
||||||
|
# CertManager is installed by default; skip with:
|
||||||
|
# - CERT_MANAGER_INSTALL_SKIP=true
|
||||||
|
KIND_CLUSTER ?= postgresql-tenant-operator-test-e2e
|
||||||
|
|
||||||
|
.PHONY: setup-test-e2e
|
||||||
|
setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
|
||||||
|
@command -v $(KIND) >/dev/null 2>&1 || { \
|
||||||
|
echo "Kind is not installed. Please install Kind manually."; \
|
||||||
|
exit 1; \
|
||||||
|
}
|
||||||
|
@case "$$($(KIND) get clusters)" in \
|
||||||
|
*"$(KIND_CLUSTER)"*) \
|
||||||
|
echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \
|
||||||
|
*) \
|
||||||
|
echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
|
||||||
|
$(KIND) create cluster --name $(KIND_CLUSTER) ;; \
|
||||||
|
esac
|
||||||
|
|
||||||
|
.PHONY: test-e2e
|
||||||
|
test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind.
|
||||||
|
KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v
|
||||||
|
$(MAKE) cleanup-test-e2e
|
||||||
|
|
||||||
|
.PHONY: cleanup-test-e2e
|
||||||
|
cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
|
||||||
|
@$(KIND) delete cluster --name $(KIND_CLUSTER)
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: golangci-lint ## Run golangci-lint linter
|
||||||
|
"$(GOLANGCI_LINT)" run
|
||||||
|
|
||||||
|
.PHONY: lint-fix
|
||||||
|
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
|
||||||
|
"$(GOLANGCI_LINT)" run --fix
|
||||||
|
|
||||||
|
.PHONY: lint-config
|
||||||
|
lint-config: golangci-lint ## Verify golangci-lint linter configuration
|
||||||
|
"$(GOLANGCI_LINT)" config verify
|
||||||
|
|
||||||
|
##@ Build
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: manifests generate fmt vet ## Build manager binary.
|
||||||
|
go build -o bin/manager cmd/main.go
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
|
run: manifests generate fmt vet ## Run a controller from your host.
|
||||||
|
go run ./cmd/main.go
|
||||||
|
|
||||||
|
# If you wish to build the manager image targeting other platforms you can use the --platform flag.
|
||||||
|
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
|
||||||
|
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
|
||||||
|
.PHONY: docker-build
|
||||||
|
docker-build: ## Build docker image with the manager.
|
||||||
|
$(CONTAINER_TOOL) build -t ${IMG} .
|
||||||
|
|
||||||
|
.PHONY: docker-push
|
||||||
|
docker-push: ## Push docker image with the manager.
|
||||||
|
$(CONTAINER_TOOL) push ${IMG}
|
||||||
|
|
||||||
|
# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
|
||||||
|
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
|
||||||
|
# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
|
||||||
|
# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
|
||||||
|
# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=<myregistry/image:<tag>> then the export will fail)
|
||||||
|
# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
|
||||||
|
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
|
||||||
|
.PHONY: docker-buildx
|
||||||
|
docker-buildx: ## Build and push docker image for the manager for cross-platform support
|
||||||
|
# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
|
||||||
|
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
|
||||||
|
- $(CONTAINER_TOOL) buildx create --name postgresql-tenant-operator-builder
|
||||||
|
$(CONTAINER_TOOL) buildx use postgresql-tenant-operator-builder
|
||||||
|
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
|
||||||
|
- $(CONTAINER_TOOL) buildx rm postgresql-tenant-operator-builder
|
||||||
|
rm Dockerfile.cross
|
||||||
|
|
||||||
|
.PHONY: build-installer
|
||||||
|
build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
|
||||||
|
mkdir -p dist
|
||||||
|
cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
|
||||||
|
"$(KUSTOMIZE)" build config/default > dist/install.yaml
|
||||||
|
|
||||||
|
##@ Deployment
|
||||||
|
|
||||||
|
ifndef ignore-not-found
|
||||||
|
ignore-not-found = false
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: install
|
||||||
|
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
|
||||||
|
@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
|
||||||
|
if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi
|
||||||
|
|
||||||
|
.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.
|
||||||
|
@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
|
||||||
|
if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi
|
||||||
|
|
||||||
|
.PHONY: deploy
|
||||||
|
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
|
||||||
|
cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
|
||||||
|
"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f -
|
||||||
|
|
||||||
|
.PHONY: undeploy
|
||||||
|
undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
|
||||||
|
"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -
|
||||||
|
|
||||||
|
##@ Dependencies
|
||||||
|
|
||||||
|
## Location to install dependencies to
|
||||||
|
LOCALBIN ?= $(shell pwd)/bin
|
||||||
|
$(LOCALBIN):
|
||||||
|
mkdir -p "$(LOCALBIN)"
|
||||||
|
|
||||||
|
## Tool Binaries
|
||||||
|
KUBECTL ?= kubectl
|
||||||
|
KIND ?= kind
|
||||||
|
KUSTOMIZE ?= $(LOCALBIN)/kustomize
|
||||||
|
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
|
||||||
|
ENVTEST ?= $(LOCALBIN)/setup-envtest
|
||||||
|
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
|
||||||
|
|
||||||
|
## Tool Versions
|
||||||
|
KUSTOMIZE_VERSION ?= v5.8.1
|
||||||
|
CONTROLLER_TOOLS_VERSION ?= v0.21.0
|
||||||
|
|
||||||
|
#ENVTEST_VERSION is the controller-runtime version to use for setup-envtest, derived from go.mod
|
||||||
|
ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \
|
||||||
|
[ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \
|
||||||
|
printf '%s\n' "$$v")
|
||||||
|
|
||||||
|
#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
|
||||||
|
ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \
|
||||||
|
[ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \
|
||||||
|
printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/')
|
||||||
|
|
||||||
|
GOLANGCI_LINT_VERSION ?= v2.12.2
|
||||||
|
.PHONY: kustomize
|
||||||
|
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
|
||||||
|
$(KUSTOMIZE): $(LOCALBIN)
|
||||||
|
$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))
|
||||||
|
|
||||||
|
.PHONY: controller-gen
|
||||||
|
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
|
||||||
|
$(CONTROLLER_GEN): $(LOCALBIN)
|
||||||
|
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))
|
||||||
|
|
||||||
|
.PHONY: setup-envtest
|
||||||
|
setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
|
||||||
|
@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
|
||||||
|
@"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \
|
||||||
|
echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
|
||||||
|
exit 1; \
|
||||||
|
}
|
||||||
|
|
||||||
|
.PHONY: envtest
|
||||||
|
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
|
||||||
|
$(ENVTEST): $(LOCALBIN)
|
||||||
|
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))
|
||||||
|
|
||||||
|
.PHONY: golangci-lint
|
||||||
|
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
|
||||||
|
$(GOLANGCI_LINT): $(LOCALBIN)
|
||||||
|
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
|
||||||
|
@test -f .custom-gcl.yml && { \
|
||||||
|
echo "Building custom golangci-lint with plugins..." && \
|
||||||
|
$(GOLANGCI_LINT) custom --destination $(LOCALBIN) --name golangci-lint-custom && \
|
||||||
|
mv -f $(LOCALBIN)/golangci-lint-custom $(GOLANGCI_LINT); \
|
||||||
|
} || true
|
||||||
|
|
||||||
|
# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
|
||||||
|
# $1 - target path with name of binary
|
||||||
|
# $2 - package url which can be installed
|
||||||
|
# $3 - specific version of package
|
||||||
|
define go-install-tool
|
||||||
|
@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \
|
||||||
|
set -e; \
|
||||||
|
package=$(2)@$(3) ;\
|
||||||
|
echo "Downloading $${package}" ;\
|
||||||
|
rm -f "$(1)" ;\
|
||||||
|
GOBIN="$(LOCALBIN)" go install $${package} ;\
|
||||||
|
mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\
|
||||||
|
} ;\
|
||||||
|
ln -sf "$$(realpath "$(1)-$(3)")" "$(1)"
|
||||||
|
endef
|
||||||
|
|
||||||
|
define gomodver
|
||||||
|
$(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null)
|
||||||
|
endef
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# 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
|
||||||
|
cliVersion: 4.15.0
|
||||||
|
domain: ddupan.top
|
||||||
|
layout:
|
||||||
|
- go.kubebuilder.io/v4
|
||||||
|
projectName: postgresql-tenant-operator
|
||||||
|
repo: git.ddupan.top/panxiao81/postgresql-tenant-operator
|
||||||
|
resources:
|
||||||
|
- api:
|
||||||
|
crdVersion: v1
|
||||||
|
controller: true
|
||||||
|
domain: ddupan.top
|
||||||
|
group: database
|
||||||
|
kind: PostgreSQLInstance
|
||||||
|
path: git.ddupan.top/panxiao81/postgresql-tenant-operator/api/v1alpha1
|
||||||
|
version: v1alpha1
|
||||||
|
- api:
|
||||||
|
crdVersion: v1
|
||||||
|
namespaced: true
|
||||||
|
controller: true
|
||||||
|
domain: ddupan.top
|
||||||
|
group: database
|
||||||
|
kind: PostgreSQLTenant
|
||||||
|
path: git.ddupan.top/panxiao81/postgresql-tenant-operator/api/v1alpha1
|
||||||
|
version: v1alpha1
|
||||||
|
version: "3"
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# postgresql-tenant-operator
|
||||||
|
|
||||||
|
面向 homelab 的 Kubernetes-native PostgreSQL 租户控制器。它不负责运行
|
||||||
|
PostgreSQL,而是把外部 PostgreSQL 实例中的 database、role、权限、扩展和
|
||||||
|
OpenBao 凭据生命周期暴露为 Kubernetes API。
|
||||||
|
|
||||||
|
项目目前处于 API 骨架阶段,尚未对 PostgreSQL 或 OpenBao 执行写操作。
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
- 通过 `PostgreSQLInstance` 注册可管理的外部 PostgreSQL 实例。
|
||||||
|
- 通过 namespaced `PostgreSQLTenant` 声明 database、owner、login 和扩展。
|
||||||
|
- 生成的密码只写入 OpenBao,不写入 CR、Event 或日志。
|
||||||
|
- 使用 `status.conditions` 暴露持续 reconcile 的结果。
|
||||||
|
- 默认使用 `Retain` 删除策略,避免删除 CR 时意外删除数据。
|
||||||
|
- 允许 GitOps、Terraform、`kubectl` 和未来的 Backstage 使用同一套 API。
|
||||||
|
|
||||||
|
## API 示例
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: database.ddupan.top/v1alpha1
|
||||||
|
kind: PostgreSQLTenant
|
||||||
|
metadata:
|
||||||
|
name: netbox
|
||||||
|
namespace: netbox
|
||||||
|
spec:
|
||||||
|
instanceRef: shared
|
||||||
|
database: netbox
|
||||||
|
ownerRole: netbox_owner
|
||||||
|
loginRole: netbox
|
||||||
|
extensions: [pg_trgm]
|
||||||
|
credential:
|
||||||
|
openBaoPath: kv/k8s/netbox/database
|
||||||
|
deletionPolicy: Retain
|
||||||
|
```
|
||||||
|
|
||||||
|
更完整的资源见 [`config/samples`](config/samples),初始架构和安全边界见
|
||||||
|
[`docs/architecture.md`](docs/architecture.md)。
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
需要 Go、Docker、`kubectl` 和一个可用于测试的 Kubernetes 集群。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make manifests generate
|
||||||
|
make test
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
安装 CRD 或在本机运行 controller:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make install
|
||||||
|
make run
|
||||||
|
```
|
||||||
|
|
||||||
|
默认 e2e 测试会创建和删除 Kind 集群,不要让它指向日常使用的集群。
|
||||||
|
|
||||||
|
## 路线
|
||||||
|
|
||||||
|
1. 完成 CRD 校验、默认值和状态约定。
|
||||||
|
2. 抽象 PostgreSQL 与 OpenBao client,用 fake 实现测试 reconciliation。
|
||||||
|
3. 实现 database、owner role、login role、grant 和 extension 的幂等收敛。
|
||||||
|
4. 实现带 finalizer 的 `Retain` / `Delete` 删除流程。
|
||||||
|
|
||||||
|
代码和 Gitea Actions 工作流预期托管在
|
||||||
|
`git.ddupan.top/panxiao81/postgresql-tenant-operator`。
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
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 database v1alpha1 API group.
|
||||||
|
// +kubebuilder:object:generate=true
|
||||||
|
// +groupName=database.ddupan.top
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// SchemeGroupVersion is group version used to register these objects.
|
||||||
|
// This name is used by applyconfiguration generators (e.g. controller-gen).
|
||||||
|
SchemeGroupVersion = schema.GroupVersion{Group: "database.ddupan.top", Version: "v1alpha1"}
|
||||||
|
|
||||||
|
// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
|
||||||
|
GroupVersion = SchemeGroupVersion
|
||||||
|
|
||||||
|
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
|
||||||
|
SchemeBuilder = runtime.NewSchemeBuilder(func(scheme *runtime.Scheme) error {
|
||||||
|
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// AddToScheme adds the types in this group-version to the given scheme.
|
||||||
|
AddToScheme = SchemeBuilder.AddToScheme
|
||||||
|
)
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
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"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// PostgreSQLInstanceSpec defines the desired state of PostgreSQLInstance
|
||||||
|
type PostgreSQLInstanceSpec struct {
|
||||||
|
// Endpoint is the PostgreSQL server managed by this instance.
|
||||||
|
// +required
|
||||||
|
Endpoint PostgreSQLEndpoint `json:"endpoint"`
|
||||||
|
|
||||||
|
// AdminCredentialRef points to an OpenBao KV secret containing the
|
||||||
|
// administrative login. Secret values are never copied into this resource.
|
||||||
|
// +required
|
||||||
|
AdminCredentialRef OpenBaoSecretReference `json:"adminCredentialRef"`
|
||||||
|
|
||||||
|
// AllowedExtensions is the allowlist tenants may request.
|
||||||
|
// +listType=set
|
||||||
|
// +optional
|
||||||
|
AllowedExtensions []string `json:"allowedExtensions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQLEndpoint identifies a PostgreSQL server.
|
||||||
|
type PostgreSQLEndpoint struct {
|
||||||
|
// +required
|
||||||
|
Host string `json:"host"`
|
||||||
|
|
||||||
|
// +kubebuilder:default=5432
|
||||||
|
// +kubebuilder:validation:Minimum=1
|
||||||
|
// +kubebuilder:validation:Maximum=65535
|
||||||
|
// +optional
|
||||||
|
Port int32 `json:"port,omitempty"`
|
||||||
|
|
||||||
|
// Database used for administrative connections.
|
||||||
|
// +kubebuilder:default=postgres
|
||||||
|
// +optional
|
||||||
|
Database string `json:"database,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Enum=disable;require;verify-ca;verify-full
|
||||||
|
// +kubebuilder:default=verify-full
|
||||||
|
// +optional
|
||||||
|
SSLMode string `json:"sslMode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenBaoSecretReference identifies keys in an OpenBao KV secret.
|
||||||
|
type OpenBaoSecretReference struct {
|
||||||
|
// +required
|
||||||
|
Path string `json:"path"`
|
||||||
|
|
||||||
|
// +kubebuilder:default=username
|
||||||
|
// +optional
|
||||||
|
UsernameKey string `json:"usernameKey,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:default=password
|
||||||
|
// +optional
|
||||||
|
PasswordKey string `json:"passwordKey,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQLInstanceStatus defines the observed state of PostgreSQLInstance.
|
||||||
|
type PostgreSQLInstanceStatus struct {
|
||||||
|
// ObservedGeneration is the most recent generation observed by the controller.
|
||||||
|
// +optional
|
||||||
|
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||||
|
|
||||||
|
// PostgreSQLVersion is reported by the target server.
|
||||||
|
// +optional
|
||||||
|
PostgreSQLVersion string `json:"postgresqlVersion,omitempty"`
|
||||||
|
|
||||||
|
// conditions represent the current state of the PostgreSQLInstance resource.
|
||||||
|
// Each condition has a unique type and reflects the status of a specific aspect of the resource.
|
||||||
|
//
|
||||||
|
// Standard condition types include:
|
||||||
|
// - "Available": the resource is fully functional
|
||||||
|
// - "Progressing": the resource is being created or updated
|
||||||
|
// - "Degraded": the resource failed to reach or maintain its desired state
|
||||||
|
//
|
||||||
|
// The status of each condition is one of True, False, or Unknown.
|
||||||
|
// +listType=map
|
||||||
|
// +listMapKey=type
|
||||||
|
// +optional
|
||||||
|
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
// +kubebuilder:subresource:status
|
||||||
|
// +kubebuilder:resource:scope=Cluster,shortName=pginstance
|
||||||
|
// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint.host`
|
||||||
|
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
|
||||||
|
// +kubebuilder:resource:scope=Cluster
|
||||||
|
|
||||||
|
// PostgreSQLInstance is the Schema for the postgresqlinstances API
|
||||||
|
type PostgreSQLInstance struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
|
||||||
|
// metadata is a standard object metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitzero"`
|
||||||
|
|
||||||
|
// spec defines the desired state of PostgreSQLInstance
|
||||||
|
// +required
|
||||||
|
Spec PostgreSQLInstanceSpec `json:"spec"`
|
||||||
|
|
||||||
|
// status defines the observed state of PostgreSQLInstance
|
||||||
|
// +optional
|
||||||
|
Status PostgreSQLInstanceStatus `json:"status,omitzero"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
|
||||||
|
// PostgreSQLInstanceList contains a list of PostgreSQLInstance
|
||||||
|
type PostgreSQLInstanceList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitzero"`
|
||||||
|
Items []PostgreSQLInstance `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||||
|
s.AddKnownTypes(SchemeGroupVersion, &PostgreSQLInstance{}, &PostgreSQLInstanceList{})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
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"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// PostgreSQLTenantSpec defines the desired state of PostgreSQLTenant
|
||||||
|
type PostgreSQLTenantSpec struct {
|
||||||
|
// InstanceRef names the cluster-scoped PostgreSQLInstance to use.
|
||||||
|
// +required
|
||||||
|
InstanceRef string `json:"instanceRef"`
|
||||||
|
|
||||||
|
// Database defaults to metadata.name when omitted.
|
||||||
|
// +optional
|
||||||
|
Database string `json:"database,omitempty"`
|
||||||
|
|
||||||
|
// OwnerRole defaults to <database>_owner when omitted.
|
||||||
|
// +optional
|
||||||
|
OwnerRole string `json:"ownerRole,omitempty"`
|
||||||
|
|
||||||
|
// LoginRole defaults to metadata.name when omitted.
|
||||||
|
// +optional
|
||||||
|
LoginRole string `json:"loginRole,omitempty"`
|
||||||
|
|
||||||
|
// Extensions to install from the instance allowlist.
|
||||||
|
// +listType=set
|
||||||
|
// +optional
|
||||||
|
Extensions []string `json:"extensions,omitempty"`
|
||||||
|
|
||||||
|
// Credential configures the application login credential.
|
||||||
|
// +required
|
||||||
|
Credential PostgreSQLCredentialSpec `json:"credential"`
|
||||||
|
|
||||||
|
// DeletionPolicy controls whether deleting this object removes the database.
|
||||||
|
// +kubebuilder:validation:Enum=Retain;Delete
|
||||||
|
// +kubebuilder:default=Retain
|
||||||
|
// +optional
|
||||||
|
DeletionPolicy string `json:"deletionPolicy,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQLCredentialSpec describes where credentials live and when to rotate them.
|
||||||
|
type PostgreSQLCredentialSpec struct {
|
||||||
|
// OpenBaoPath is the KV path receiving the generated login credential.
|
||||||
|
// +required
|
||||||
|
OpenBaoPath string `json:"openBaoPath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQLTenantStatus defines the observed state of PostgreSQLTenant.
|
||||||
|
type PostgreSQLTenantStatus struct {
|
||||||
|
// ObservedGeneration is the most recent generation observed by the controller.
|
||||||
|
// +optional
|
||||||
|
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||||
|
|
||||||
|
// DatabaseOID is the server-side identity observed for the database.
|
||||||
|
// +optional
|
||||||
|
DatabaseOID uint32 `json:"databaseOID,omitempty"`
|
||||||
|
|
||||||
|
// conditions represent the current state of the PostgreSQLTenant resource.
|
||||||
|
// Each condition has a unique type and reflects the status of a specific aspect of the resource.
|
||||||
|
//
|
||||||
|
// Standard condition types include:
|
||||||
|
// - "Available": the resource is fully functional
|
||||||
|
// - "Progressing": the resource is being created or updated
|
||||||
|
// - "Degraded": the resource failed to reach or maintain its desired state
|
||||||
|
//
|
||||||
|
// The status of each condition is one of True, False, or Unknown.
|
||||||
|
// +listType=map
|
||||||
|
// +listMapKey=type
|
||||||
|
// +optional
|
||||||
|
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
// +kubebuilder:subresource:status
|
||||||
|
// +kubebuilder:resource:scope=Namespaced,shortName=pgtenant
|
||||||
|
// +kubebuilder:printcolumn:name="Instance",type=string,JSONPath=`.spec.instanceRef`
|
||||||
|
// +kubebuilder:printcolumn:name="Database",type=string,JSONPath=`.spec.database`
|
||||||
|
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
|
||||||
|
|
||||||
|
// PostgreSQLTenant is the Schema for the postgresqltenants API
|
||||||
|
type PostgreSQLTenant struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
|
||||||
|
// metadata is a standard object metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitzero"`
|
||||||
|
|
||||||
|
// spec defines the desired state of PostgreSQLTenant
|
||||||
|
// +required
|
||||||
|
Spec PostgreSQLTenantSpec `json:"spec"`
|
||||||
|
|
||||||
|
// status defines the observed state of PostgreSQLTenant
|
||||||
|
// +optional
|
||||||
|
Status PostgreSQLTenantStatus `json:"status,omitzero"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
|
||||||
|
// PostgreSQLTenantList contains a list of PostgreSQLTenant
|
||||||
|
type PostgreSQLTenantList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitzero"`
|
||||||
|
Items []PostgreSQLTenant `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||||
|
s.AddKnownTypes(SchemeGroupVersion, &PostgreSQLTenant{}, &PostgreSQLTenantList{})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
//go:build !ignore_autogenerated
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
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"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *OpenBaoSecretReference) DeepCopyInto(out *OpenBaoSecretReference) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenBaoSecretReference.
|
||||||
|
func (in *OpenBaoSecretReference) DeepCopy() *OpenBaoSecretReference {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(OpenBaoSecretReference)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *PostgreSQLCredentialSpec) DeepCopyInto(out *PostgreSQLCredentialSpec) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLCredentialSpec.
|
||||||
|
func (in *PostgreSQLCredentialSpec) DeepCopy() *PostgreSQLCredentialSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PostgreSQLCredentialSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *PostgreSQLEndpoint) DeepCopyInto(out *PostgreSQLEndpoint) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLEndpoint.
|
||||||
|
func (in *PostgreSQLEndpoint) DeepCopy() *PostgreSQLEndpoint {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PostgreSQLEndpoint)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *PostgreSQLInstance) DeepCopyInto(out *PostgreSQLInstance) {
|
||||||
|
*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 PostgreSQLInstance.
|
||||||
|
func (in *PostgreSQLInstance) DeepCopy() *PostgreSQLInstance {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PostgreSQLInstance)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *PostgreSQLInstance) 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 *PostgreSQLInstanceList) DeepCopyInto(out *PostgreSQLInstanceList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]PostgreSQLInstance, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLInstanceList.
|
||||||
|
func (in *PostgreSQLInstanceList) DeepCopy() *PostgreSQLInstanceList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PostgreSQLInstanceList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *PostgreSQLInstanceList) 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 *PostgreSQLInstanceSpec) DeepCopyInto(out *PostgreSQLInstanceSpec) {
|
||||||
|
*out = *in
|
||||||
|
out.Endpoint = in.Endpoint
|
||||||
|
out.AdminCredentialRef = in.AdminCredentialRef
|
||||||
|
if in.AllowedExtensions != nil {
|
||||||
|
in, out := &in.AllowedExtensions, &out.AllowedExtensions
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLInstanceSpec.
|
||||||
|
func (in *PostgreSQLInstanceSpec) DeepCopy() *PostgreSQLInstanceSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PostgreSQLInstanceSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *PostgreSQLInstanceStatus) DeepCopyInto(out *PostgreSQLInstanceStatus) {
|
||||||
|
*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 PostgreSQLInstanceStatus.
|
||||||
|
func (in *PostgreSQLInstanceStatus) DeepCopy() *PostgreSQLInstanceStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PostgreSQLInstanceStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *PostgreSQLTenant) DeepCopyInto(out *PostgreSQLTenant) {
|
||||||
|
*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 PostgreSQLTenant.
|
||||||
|
func (in *PostgreSQLTenant) DeepCopy() *PostgreSQLTenant {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PostgreSQLTenant)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *PostgreSQLTenant) 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 *PostgreSQLTenantList) DeepCopyInto(out *PostgreSQLTenantList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]PostgreSQLTenant, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLTenantList.
|
||||||
|
func (in *PostgreSQLTenantList) DeepCopy() *PostgreSQLTenantList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PostgreSQLTenantList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *PostgreSQLTenantList) 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 *PostgreSQLTenantSpec) DeepCopyInto(out *PostgreSQLTenantSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.Extensions != nil {
|
||||||
|
in, out := &in.Extensions, &out.Extensions
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
out.Credential = in.Credential
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLTenantSpec.
|
||||||
|
func (in *PostgreSQLTenantSpec) DeepCopy() *PostgreSQLTenantSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PostgreSQLTenantSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *PostgreSQLTenantStatus) DeepCopyInto(out *PostgreSQLTenantStatus) {
|
||||||
|
*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 PostgreSQLTenantStatus.
|
||||||
|
func (in *PostgreSQLTenantStatus) DeepCopy() *PostgreSQLTenantStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PostgreSQLTenantStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
+211
@@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||||
|
// to ensure that exec-entrypoint and run can make use of them.
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||||
|
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||||
|
|
||||||
|
databasev1alpha1 "git.ddupan.top/panxiao81/postgresql-tenant-operator/api/v1alpha1"
|
||||||
|
"git.ddupan.top/panxiao81/postgresql-tenant-operator/internal/controller"
|
||||||
|
// +kubebuilder:scaffold:imports
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scheme = runtime.NewScheme()
|
||||||
|
setupLog = ctrl.Log.WithName("setup")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||||
|
|
||||||
|
utilruntime.Must(databasev1alpha1.AddToScheme(scheme))
|
||||||
|
// +kubebuilder:scaffold:scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:gocyclo
|
||||||
|
func main() {
|
||||||
|
var metricsAddr string
|
||||||
|
var metricsCertPath, metricsCertName, metricsCertKey string
|
||||||
|
var webhookCertPath, webhookCertName, webhookCertKey string
|
||||||
|
var enableLeaderElection bool
|
||||||
|
var probeAddr string
|
||||||
|
var secureMetrics bool
|
||||||
|
var enableHTTP2 bool
|
||||||
|
var tlsOpts []func(*tls.Config)
|
||||||
|
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||||
|
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||||
|
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||||
|
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||||
|
"Enable leader election for controller manager. "+
|
||||||
|
"Enabling this will ensure there is only one active controller manager.")
|
||||||
|
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||||
|
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||||
|
flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
|
||||||
|
flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
|
||||||
|
flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
|
||||||
|
flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
|
||||||
|
"The directory that contains the metrics server certificate.")
|
||||||
|
flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
|
||||||
|
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
|
||||||
|
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||||
|
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||||
|
opts := zap.Options{
|
||||||
|
Development: true,
|
||||||
|
}
|
||||||
|
opts.BindFlags(flag.CommandLine)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||||
|
|
||||||
|
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||||
|
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||||
|
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||||
|
// Rapid Reset CVEs. For more information see:
|
||||||
|
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||||
|
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||||
|
disableHTTP2 := func(c *tls.Config) {
|
||||||
|
setupLog.Info("Disabling HTTP/2")
|
||||||
|
c.NextProtos = []string{"http/1.1"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enableHTTP2 {
|
||||||
|
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial webhook TLS options
|
||||||
|
webhookTLSOpts := tlsOpts
|
||||||
|
webhookServerOptions := webhook.Options{
|
||||||
|
TLSOpts: webhookTLSOpts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(webhookCertPath) > 0 {
|
||||||
|
setupLog.Info("Initializing webhook certificate watcher using provided certificates",
|
||||||
|
"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)
|
||||||
|
|
||||||
|
webhookServerOptions.CertDir = webhookCertPath
|
||||||
|
webhookServerOptions.CertName = webhookCertName
|
||||||
|
webhookServerOptions.KeyName = webhookCertKey
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookServer := webhook.NewServer(webhookServerOptions)
|
||||||
|
|
||||||
|
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||||
|
// More info:
|
||||||
|
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/metrics/server
|
||||||
|
// - https://book.kubebuilder.io/reference/metrics.html
|
||||||
|
metricsServerOptions := metricsserver.Options{
|
||||||
|
BindAddress: metricsAddr,
|
||||||
|
SecureServing: secureMetrics,
|
||||||
|
TLSOpts: tlsOpts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if secureMetrics {
|
||||||
|
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||||
|
// These configurations ensure that only authorized users and service accounts
|
||||||
|
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||||
|
// https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||||
|
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the certificate is not specified, controller-runtime will automatically
|
||||||
|
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||||
|
// this setup is not recommended for production.
|
||||||
|
//
|
||||||
|
// TODO(user): If you enable certManager, uncomment the following lines:
|
||||||
|
// - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
|
||||||
|
// managed by cert-manager for the metrics server.
|
||||||
|
// - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
|
||||||
|
if len(metricsCertPath) > 0 {
|
||||||
|
setupLog.Info("Initializing metrics certificate watcher using provided certificates",
|
||||||
|
"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)
|
||||||
|
|
||||||
|
metricsServerOptions.CertDir = metricsCertPath
|
||||||
|
metricsServerOptions.CertName = metricsCertName
|
||||||
|
metricsServerOptions.KeyName = metricsCertKey
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||||
|
Scheme: scheme,
|
||||||
|
Metrics: metricsServerOptions,
|
||||||
|
WebhookServer: webhookServer,
|
||||||
|
HealthProbeBindAddress: probeAddr,
|
||||||
|
LeaderElection: enableLeaderElection,
|
||||||
|
LeaderElectionID: "e106ad39.ddupan.top",
|
||||||
|
// 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, "Failed to start manager")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := (&controller.PostgreSQLInstanceReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
setupLog.Error(err, "Failed to create controller", "controller", "postgresqlinstance")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := (&controller.PostgreSQLTenantReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
setupLog.Error(err, "Failed to create controller", "controller", "postgresqltenant")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// +kubebuilder:scaffold:builder
|
||||||
|
|
||||||
|
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||||
|
setupLog.Error(err, "Failed to set up health check")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||||
|
setupLog.Error(err, "Failed to set up ready check")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupLog.Info("Starting manager")
|
||||||
|
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||||
|
setupLog.Error(err, "Failed to run manager")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.21.0
|
||||||
|
name: postgresqlinstances.database.ddupan.top
|
||||||
|
spec:
|
||||||
|
group: database.ddupan.top
|
||||||
|
names:
|
||||||
|
kind: PostgreSQLInstance
|
||||||
|
listKind: PostgreSQLInstanceList
|
||||||
|
plural: postgresqlinstances
|
||||||
|
singular: postgresqlinstance
|
||||||
|
scope: Cluster
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.endpoint.host
|
||||||
|
name: Endpoint
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||||
|
name: Ready
|
||||||
|
type: string
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: PostgreSQLInstance is the Schema for the postgresqlinstances
|
||||||
|
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: spec defines the desired state of PostgreSQLInstance
|
||||||
|
properties:
|
||||||
|
adminCredentialRef:
|
||||||
|
description: |-
|
||||||
|
AdminCredentialRef points to an OpenBao KV secret containing the
|
||||||
|
administrative login. Secret values are never copied into this resource.
|
||||||
|
properties:
|
||||||
|
passwordKey:
|
||||||
|
default: password
|
||||||
|
type: string
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
usernameKey:
|
||||||
|
default: username
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- path
|
||||||
|
type: object
|
||||||
|
allowedExtensions:
|
||||||
|
description: AllowedExtensions is the allowlist tenants may request.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: set
|
||||||
|
endpoint:
|
||||||
|
description: Endpoint is the PostgreSQL server managed by this instance.
|
||||||
|
properties:
|
||||||
|
database:
|
||||||
|
default: postgres
|
||||||
|
description: Database used for administrative connections.
|
||||||
|
type: string
|
||||||
|
host:
|
||||||
|
type: string
|
||||||
|
port:
|
||||||
|
default: 5432
|
||||||
|
format: int32
|
||||||
|
maximum: 65535
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
sslMode:
|
||||||
|
default: verify-full
|
||||||
|
enum:
|
||||||
|
- disable
|
||||||
|
- require
|
||||||
|
- verify-ca
|
||||||
|
- verify-full
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- host
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- adminCredentialRef
|
||||||
|
- endpoint
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: status defines the observed state of PostgreSQLInstance
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
description: |-
|
||||||
|
conditions represent the current state of the PostgreSQLInstance resource.
|
||||||
|
Each condition has a unique type and reflects the status of a specific aspect of the resource.
|
||||||
|
|
||||||
|
Standard condition types include:
|
||||||
|
- "Available": the resource is fully functional
|
||||||
|
- "Progressing": the resource is being created or updated
|
||||||
|
- "Degraded": the resource failed to reach or maintain its desired state
|
||||||
|
|
||||||
|
The status of each condition is one of True, False, or Unknown.
|
||||||
|
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
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
observedGeneration:
|
||||||
|
description: ObservedGeneration is the most recent generation observed
|
||||||
|
by the controller.
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
postgresqlVersion:
|
||||||
|
description: PostgreSQLVersion is reported by the target server.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- spec
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.21.0
|
||||||
|
name: postgresqltenants.database.ddupan.top
|
||||||
|
spec:
|
||||||
|
group: database.ddupan.top
|
||||||
|
names:
|
||||||
|
kind: PostgreSQLTenant
|
||||||
|
listKind: PostgreSQLTenantList
|
||||||
|
plural: postgresqltenants
|
||||||
|
shortNames:
|
||||||
|
- pgtenant
|
||||||
|
singular: postgresqltenant
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.instanceRef
|
||||||
|
name: Instance
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.database
|
||||||
|
name: Database
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||||
|
name: Ready
|
||||||
|
type: string
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: PostgreSQLTenant is the Schema for the postgresqltenants 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: spec defines the desired state of PostgreSQLTenant
|
||||||
|
properties:
|
||||||
|
credential:
|
||||||
|
description: Credential configures the application login credential.
|
||||||
|
properties:
|
||||||
|
openBaoPath:
|
||||||
|
description: OpenBaoPath is the KV path receiving the generated
|
||||||
|
login credential.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- openBaoPath
|
||||||
|
type: object
|
||||||
|
database:
|
||||||
|
description: Database defaults to metadata.name when omitted.
|
||||||
|
type: string
|
||||||
|
deletionPolicy:
|
||||||
|
default: Retain
|
||||||
|
description: DeletionPolicy controls whether deleting this object
|
||||||
|
removes the database.
|
||||||
|
enum:
|
||||||
|
- Retain
|
||||||
|
- Delete
|
||||||
|
type: string
|
||||||
|
extensions:
|
||||||
|
description: Extensions to install from the instance allowlist.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: set
|
||||||
|
instanceRef:
|
||||||
|
description: InstanceRef names the cluster-scoped PostgreSQLInstance
|
||||||
|
to use.
|
||||||
|
type: string
|
||||||
|
loginRole:
|
||||||
|
description: LoginRole defaults to metadata.name when omitted.
|
||||||
|
type: string
|
||||||
|
ownerRole:
|
||||||
|
description: OwnerRole defaults to <database>_owner when omitted.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- credential
|
||||||
|
- instanceRef
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: status defines the observed state of PostgreSQLTenant
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
description: |-
|
||||||
|
conditions represent the current state of the PostgreSQLTenant resource.
|
||||||
|
Each condition has a unique type and reflects the status of a specific aspect of the resource.
|
||||||
|
|
||||||
|
Standard condition types include:
|
||||||
|
- "Available": the resource is fully functional
|
||||||
|
- "Progressing": the resource is being created or updated
|
||||||
|
- "Degraded": the resource failed to reach or maintain its desired state
|
||||||
|
|
||||||
|
The status of each condition is one of True, False, or Unknown.
|
||||||
|
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
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
databaseOID:
|
||||||
|
description: DatabaseOID is the server-side identity observed for
|
||||||
|
the database.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
observedGeneration:
|
||||||
|
description: ObservedGeneration is the most recent generation observed
|
||||||
|
by the controller.
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- spec
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# 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/database.ddupan.top_postgresqlinstances.yaml
|
||||||
|
- bases/database.ddupan.top_postgresqltenants.yaml
|
||||||
|
# +kubebuilder:scaffold:crdkustomizeresource
|
||||||
|
|
||||||
|
patches:
|
||||||
|
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
|
||||||
|
# patches here are for enabling the conversion webhook for each CRD
|
||||||
|
# +kubebuilder:scaffold:crdkustomizewebhookpatch
|
||||||
|
|
||||||
|
# [WEBHOOK] To enable webhook, uncomment the following section
|
||||||
|
# the following config is for teaching kustomize how to do kustomization for CRDs.
|
||||||
|
#configurations:
|
||||||
|
#- kustomizeconfig.yaml
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
varReference:
|
||||||
|
- path: metadata/annotations
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs.
|
||||||
|
|
||||||
|
# Add the volumeMount for the metrics-server certs
|
||||||
|
- op: add
|
||||||
|
path: /spec/template/spec/containers/0/volumeMounts/-
|
||||||
|
value:
|
||||||
|
mountPath: /tmp/k8s-metrics-server/metrics-certs
|
||||||
|
name: metrics-certs
|
||||||
|
readOnly: true
|
||||||
|
|
||||||
|
# Add the --metrics-cert-path argument for the metrics server
|
||||||
|
- op: add
|
||||||
|
path: /spec/template/spec/containers/0/args/-
|
||||||
|
value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs
|
||||||
|
|
||||||
|
# Add the metrics-server certs volume configuration
|
||||||
|
- op: add
|
||||||
|
path: /spec/template/spec/volumes/-
|
||||||
|
value:
|
||||||
|
name: metrics-certs
|
||||||
|
secret:
|
||||||
|
secretName: metrics-server-cert
|
||||||
|
optional: false
|
||||||
|
items:
|
||||||
|
- key: ca.crt
|
||||||
|
path: ca.crt
|
||||||
|
- key: tls.crt
|
||||||
|
path: tls.crt
|
||||||
|
- key: tls.key
|
||||||
|
path: tls.key
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
# Adds namespace to all resources.
|
||||||
|
namespace: postgresql-tenant-operator-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: postgresql-tenant-operator-
|
||||||
|
|
||||||
|
# Labels to add to all resources and selectors.
|
||||||
|
#labels:
|
||||||
|
#- includeSelectors: true
|
||||||
|
# pairs:
|
||||||
|
# someName: someValue
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- ../crd
|
||||||
|
- ../rbac
|
||||||
|
- ../manager
|
||||||
|
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
||||||
|
# crd/kustomization.yaml
|
||||||
|
#- ../webhook
|
||||||
|
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
|
||||||
|
#- ../certmanager
|
||||||
|
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
|
||||||
|
#- ../prometheus
|
||||||
|
# [METRICS] Expose the controller manager metrics service.
|
||||||
|
- metrics_service.yaml
|
||||||
|
# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy.
|
||||||
|
# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics.
|
||||||
|
# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will
|
||||||
|
# be able to communicate with the Webhook Server.
|
||||||
|
#- ../network-policy
|
||||||
|
|
||||||
|
# Uncomment the patches line if you enable Metrics
|
||||||
|
patches:
|
||||||
|
# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
|
||||||
|
# More info: https://book.kubebuilder.io/reference/metrics
|
||||||
|
- path: manager_metrics_patch.yaml
|
||||||
|
target:
|
||||||
|
kind: Deployment
|
||||||
|
|
||||||
|
# Uncomment the patches line if you enable Metrics and CertManager
|
||||||
|
# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line.
|
||||||
|
# This patch will protect the metrics with certManager self-signed certs.
|
||||||
|
#- path: cert_metrics_manager_patch.yaml
|
||||||
|
# target:
|
||||||
|
# kind: Deployment
|
||||||
|
|
||||||
|
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
||||||
|
# crd/kustomization.yaml
|
||||||
|
#- path: manager_webhook_patch.yaml
|
||||||
|
# target:
|
||||||
|
# kind: Deployment
|
||||||
|
|
||||||
|
# [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: # Uncomment the following block to enable certificates for metrics
|
||||||
|
# kind: Service
|
||||||
|
# version: v1
|
||||||
|
# name: controller-manager-metrics-service
|
||||||
|
# fieldPath: metadata.name
|
||||||
|
# targets:
|
||||||
|
# - select:
|
||||||
|
# kind: Certificate
|
||||||
|
# group: cert-manager.io
|
||||||
|
# version: v1
|
||||||
|
# name: metrics-certs
|
||||||
|
# fieldPaths:
|
||||||
|
# - spec.dnsNames.0
|
||||||
|
# - spec.dnsNames.1
|
||||||
|
# options:
|
||||||
|
# delimiter: '.'
|
||||||
|
# index: 0
|
||||||
|
# create: true
|
||||||
|
# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor
|
||||||
|
# kind: ServiceMonitor
|
||||||
|
# group: monitoring.coreos.com
|
||||||
|
# version: v1
|
||||||
|
# name: controller-manager-metrics-monitor
|
||||||
|
# fieldPaths:
|
||||||
|
# - spec.endpoints.0.tlsConfig.serverName
|
||||||
|
# options:
|
||||||
|
# delimiter: '.'
|
||||||
|
# index: 0
|
||||||
|
# create: true
|
||||||
|
|
||||||
|
# - source:
|
||||||
|
# kind: Service
|
||||||
|
# version: v1
|
||||||
|
# name: controller-manager-metrics-service
|
||||||
|
# fieldPath: metadata.namespace
|
||||||
|
# targets:
|
||||||
|
# - select:
|
||||||
|
# kind: Certificate
|
||||||
|
# group: cert-manager.io
|
||||||
|
# version: v1
|
||||||
|
# name: metrics-certs
|
||||||
|
# fieldPaths:
|
||||||
|
# - spec.dnsNames.0
|
||||||
|
# - spec.dnsNames.1
|
||||||
|
# options:
|
||||||
|
# delimiter: '.'
|
||||||
|
# index: 1
|
||||||
|
# create: true
|
||||||
|
# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor
|
||||||
|
# kind: ServiceMonitor
|
||||||
|
# group: monitoring.coreos.com
|
||||||
|
# version: v1
|
||||||
|
# name: controller-manager-metrics-monitor
|
||||||
|
# fieldPaths:
|
||||||
|
# - spec.endpoints.0.tlsConfig.serverName
|
||||||
|
# options:
|
||||||
|
# delimiter: '.'
|
||||||
|
# index: 1
|
||||||
|
# create: true
|
||||||
|
|
||||||
|
# - source: # Uncomment the following block if you have any webhook
|
||||||
|
# kind: Service
|
||||||
|
# version: v1
|
||||||
|
# name: webhook-service
|
||||||
|
# fieldPath: .metadata.name # Name of the service
|
||||||
|
# targets:
|
||||||
|
# - select:
|
||||||
|
# kind: Certificate
|
||||||
|
# group: cert-manager.io
|
||||||
|
# version: v1
|
||||||
|
# name: serving-cert
|
||||||
|
# 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
|
||||||
|
# name: serving-cert
|
||||||
|
# fieldPaths:
|
||||||
|
# - .spec.dnsNames.0
|
||||||
|
# - .spec.dnsNames.1
|
||||||
|
# options:
|
||||||
|
# delimiter: '.'
|
||||||
|
# index: 1
|
||||||
|
# create: true
|
||||||
|
|
||||||
|
# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
|
||||||
|
# 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
|
||||||
|
# - source:
|
||||||
|
# kind: Certificate
|
||||||
|
# group: cert-manager.io
|
||||||
|
# version: v1
|
||||||
|
# name: serving-cert
|
||||||
|
# fieldPath: .metadata.name
|
||||||
|
# targets:
|
||||||
|
# - select:
|
||||||
|
# kind: ValidatingWebhookConfiguration
|
||||||
|
# fieldPaths:
|
||||||
|
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||||
|
# options:
|
||||||
|
# delimiter: '/'
|
||||||
|
# index: 1
|
||||||
|
# create: true
|
||||||
|
|
||||||
|
# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
|
||||||
|
# kind: Certificate
|
||||||
|
# group: cert-manager.io
|
||||||
|
# version: v1
|
||||||
|
# name: serving-cert
|
||||||
|
# fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||||
|
# targets:
|
||||||
|
# - select:
|
||||||
|
# kind: MutatingWebhookConfiguration
|
||||||
|
# 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
|
||||||
|
# fieldPath: .metadata.name
|
||||||
|
# targets:
|
||||||
|
# - select:
|
||||||
|
# kind: MutatingWebhookConfiguration
|
||||||
|
# fieldPaths:
|
||||||
|
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||||
|
# options:
|
||||||
|
# delimiter: '/'
|
||||||
|
# index: 1
|
||||||
|
# create: true
|
||||||
|
|
||||||
|
# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
|
||||||
|
# kind: Certificate
|
||||||
|
# group: cert-manager.io
|
||||||
|
# version: v1
|
||||||
|
# name: serving-cert
|
||||||
|
# fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||||
|
# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
|
||||||
|
# +kubebuilder:scaffold:crdkustomizecainjectionns
|
||||||
|
# - source:
|
||||||
|
# kind: Certificate
|
||||||
|
# group: cert-manager.io
|
||||||
|
# version: v1
|
||||||
|
# name: serving-cert
|
||||||
|
# fieldPath: .metadata.name
|
||||||
|
# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
|
||||||
|
# +kubebuilder:scaffold:crdkustomizecainjectionname
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
|
||||||
|
- op: add
|
||||||
|
path: /spec/template/spec/containers/0/args/0
|
||||||
|
value: --metrics-bind-address=:8443
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
control-plane: controller-manager
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: controller-manager-metrics-service
|
||||||
|
namespace: system
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: https
|
||||||
|
port: 8443
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 8443
|
||||||
|
selector:
|
||||||
|
control-plane: controller-manager
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
resources:
|
||||||
|
- manager.yaml
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
control-plane: controller-manager
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
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: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
control-plane: controller-manager
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
kubectl.kubernetes.io/default-container: manager
|
||||||
|
labels:
|
||||||
|
control-plane: controller-manager
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
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:
|
||||||
|
# Projects are configured by default to adhere to the "restricted" Pod Security Standards.
|
||||||
|
# This ensures that deployments meet the highest security requirements for Kubernetes.
|
||||||
|
# For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- command:
|
||||||
|
- /manager
|
||||||
|
args:
|
||||||
|
- --leader-elect
|
||||||
|
- --health-probe-bind-address=:8081
|
||||||
|
image: controller:latest
|
||||||
|
name: manager
|
||||||
|
ports:
|
||||||
|
- containerPort: 8081
|
||||||
|
name: health
|
||||||
|
protocol: TCP
|
||||||
|
securityContext:
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
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
|
||||||
|
volumeMounts: []
|
||||||
|
volumes: []
|
||||||
|
serviceAccountName: controller-manager
|
||||||
|
terminationGracePeriodSeconds: 10
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# This NetworkPolicy allows ingress traffic
|
||||||
|
# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those
|
||||||
|
# namespaces are able to gather data from the metrics endpoint.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: allow-metrics-traffic
|
||||||
|
namespace: system
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
control-plane: controller-manager
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
# This allows ingress traffic from any namespace with the label metrics: enabled
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
metrics: enabled # Only from namespaces with this label
|
||||||
|
ports:
|
||||||
|
- port: 8443
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
resources:
|
||||||
|
- allow-metrics-traffic.yaml
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
resources:
|
||||||
|
- monitor.yaml
|
||||||
|
|
||||||
|
# [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus
|
||||||
|
# to securely reference certificates created and managed by cert-manager.
|
||||||
|
# Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml
|
||||||
|
# to mount the "metrics-server-cert" secret in the Manager Deployment.
|
||||||
|
#patches:
|
||||||
|
# - path: monitor_tls_patch.yaml
|
||||||
|
# target:
|
||||||
|
# kind: ServiceMonitor
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Prometheus Monitor Service (Metrics)
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: ServiceMonitor
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
control-plane: controller-manager
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: controller-manager-metrics-monitor
|
||||||
|
namespace: system
|
||||||
|
spec:
|
||||||
|
endpoints:
|
||||||
|
- path: /metrics
|
||||||
|
port: https # Ensure this is the name of the port that exposes HTTPS metrics
|
||||||
|
scheme: https
|
||||||
|
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||||
|
tlsConfig:
|
||||||
|
# TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables
|
||||||
|
# certificate verification, exposing the system to potential man-in-the-middle attacks.
|
||||||
|
# For production environments, it is recommended to use cert-manager for automatic TLS certificate management.
|
||||||
|
# To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml,
|
||||||
|
# which securely references the certificate from the 'metrics-server-cert' secret.
|
||||||
|
insecureSkipVerify: true
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
control-plane: controller-manager
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Patch for Prometheus ServiceMonitor to enable secure TLS configuration
|
||||||
|
# using certificates managed by cert-manager
|
||||||
|
- op: replace
|
||||||
|
path: /spec/endpoints/0/tlsConfig
|
||||||
|
value:
|
||||||
|
# SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
|
||||||
|
serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc
|
||||||
|
insecureSkipVerify: false
|
||||||
|
ca:
|
||||||
|
secret:
|
||||||
|
name: metrics-server-cert
|
||||||
|
key: ca.crt
|
||||||
|
cert:
|
||||||
|
secret:
|
||||||
|
name: metrics-server-cert
|
||||||
|
key: tls.crt
|
||||||
|
keySecret:
|
||||||
|
name: metrics-server-cert
|
||||||
|
key: tls.key
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
resources:
|
||||||
|
# All RBAC will be applied under this service account in
|
||||||
|
# the deployment namespace. You may comment out this resource
|
||||||
|
# if your manager will use a service account that exists at
|
||||||
|
# runtime. Be sure to update RoleBinding and ClusterRoleBinding
|
||||||
|
# subjects if changing service account names.
|
||||||
|
- service_account.yaml
|
||||||
|
- role.yaml
|
||||||
|
- role_binding.yaml
|
||||||
|
- leader_election_role.yaml
|
||||||
|
- leader_election_role_binding.yaml
|
||||||
|
# The following RBAC configurations are used to protect
|
||||||
|
# the metrics endpoint with authn/authz. These configurations
|
||||||
|
# ensure that only authorized users and service accounts
|
||||||
|
# can access the metrics endpoint. Comment the following
|
||||||
|
# permissions if you want to disable this protection.
|
||||||
|
# More info: https://book.kubebuilder.io/reference/metrics.html
|
||||||
|
- metrics_auth_role.yaml
|
||||||
|
- metrics_auth_role_binding.yaml
|
||||||
|
- metrics_reader_role.yaml
|
||||||
|
# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by
|
||||||
|
# default, aiding admins in cluster management. Those roles are
|
||||||
|
# not used by the postgresql-tenant-operator itself. You can comment the following lines
|
||||||
|
# if you do not want those helpers be installed with your Project.
|
||||||
|
- postgresqltenant_admin_role.yaml
|
||||||
|
- postgresqltenant_editor_role.yaml
|
||||||
|
- postgresqltenant_viewer_role.yaml
|
||||||
|
- postgresqlinstance_admin_role.yaml
|
||||||
|
- postgresqlinstance_editor_role.yaml
|
||||||
|
- postgresqlinstance_viewer_role.yaml
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# permissions to do leader election.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
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
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
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
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: metrics-auth-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- authentication.k8s.io
|
||||||
|
resources:
|
||||||
|
- tokenreviews
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- apiGroups:
|
||||||
|
- authorization.k8s.io
|
||||||
|
resources:
|
||||||
|
- subjectaccessreviews
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: metrics-auth-rolebinding
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: metrics-auth-role
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: controller-manager
|
||||||
|
namespace: system
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: metrics-reader
|
||||||
|
rules:
|
||||||
|
- nonResourceURLs:
|
||||||
|
- "/metrics"
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# This rule is not used by the project postgresql-tenant-operator itself.
|
||||||
|
# It is provided to allow the cluster admin to help manage permissions for users.
|
||||||
|
#
|
||||||
|
# Grants full permissions ('*') over database.ddupan.top.
|
||||||
|
# This role is intended for users authorized to modify roles and bindings within the cluster,
|
||||||
|
# enabling them to delegate specific permissions to other users or groups as needed.
|
||||||
|
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: postgresqlinstance-admin-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqlinstances
|
||||||
|
verbs:
|
||||||
|
- '*'
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqlinstances/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# This rule is not used by the project postgresql-tenant-operator itself.
|
||||||
|
# It is provided to allow the cluster admin to help manage permissions for users.
|
||||||
|
#
|
||||||
|
# Grants permissions to create, update, and delete resources within the database.ddupan.top.
|
||||||
|
# This role is intended for users who need to manage these resources
|
||||||
|
# but should not control RBAC or manage permissions for others.
|
||||||
|
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: postgresqlinstance-editor-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqlinstances
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqlinstances/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# This rule is not used by the project postgresql-tenant-operator itself.
|
||||||
|
# It is provided to allow the cluster admin to help manage permissions for users.
|
||||||
|
#
|
||||||
|
# Grants read-only access to database.ddupan.top resources.
|
||||||
|
# This role is intended for users who need visibility into these resources
|
||||||
|
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
|
||||||
|
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: postgresqlinstance-viewer-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqlinstances
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqlinstances/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# This rule is not used by the project postgresql-tenant-operator itself.
|
||||||
|
# It is provided to allow the cluster admin to help manage permissions for users.
|
||||||
|
#
|
||||||
|
# Grants full permissions ('*') over database.ddupan.top.
|
||||||
|
# This role is intended for users authorized to modify roles and bindings within the cluster,
|
||||||
|
# enabling them to delegate specific permissions to other users or groups as needed.
|
||||||
|
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: postgresqltenant-admin-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqltenants
|
||||||
|
verbs:
|
||||||
|
- '*'
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqltenants/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# This rule is not used by the project postgresql-tenant-operator itself.
|
||||||
|
# It is provided to allow the cluster admin to help manage permissions for users.
|
||||||
|
#
|
||||||
|
# Grants permissions to create, update, and delete resources within the database.ddupan.top.
|
||||||
|
# This role is intended for users who need to manage these resources
|
||||||
|
# but should not control RBAC or manage permissions for others.
|
||||||
|
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: postgresqltenant-editor-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqltenants
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqltenants/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# This rule is not used by the project postgresql-tenant-operator itself.
|
||||||
|
# It is provided to allow the cluster admin to help manage permissions for users.
|
||||||
|
#
|
||||||
|
# Grants read-only access to database.ddupan.top resources.
|
||||||
|
# This role is intended for users who need visibility into these resources
|
||||||
|
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
|
||||||
|
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: postgresqltenant-viewer-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqltenants
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqltenants/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: manager-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqlinstances
|
||||||
|
- postgresqltenants
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqlinstances/finalizers
|
||||||
|
- postgresqltenants/finalizers
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- database.ddupan.top
|
||||||
|
resources:
|
||||||
|
- postgresqlinstances/status
|
||||||
|
- postgresqltenants/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: controller-manager
|
||||||
|
namespace: system
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: database.ddupan.top/v1alpha1
|
||||||
|
kind: PostgreSQLInstance
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: shared
|
||||||
|
spec:
|
||||||
|
endpoint:
|
||||||
|
host: postgres.internal
|
||||||
|
port: 5432
|
||||||
|
database: postgres
|
||||||
|
sslMode: verify-full
|
||||||
|
adminCredentialRef:
|
||||||
|
path: database/static-creds/postgresql-tenant-operator
|
||||||
|
allowedExtensions:
|
||||||
|
- pg_trgm
|
||||||
|
- vector
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: database.ddupan.top/v1alpha1
|
||||||
|
kind: PostgreSQLTenant
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: postgresql-tenant-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: netbox
|
||||||
|
namespace: netbox
|
||||||
|
spec:
|
||||||
|
instanceRef: shared
|
||||||
|
database: netbox
|
||||||
|
ownerRole: netbox_owner
|
||||||
|
loginRole: netbox
|
||||||
|
extensions:
|
||||||
|
- pg_trgm
|
||||||
|
credential:
|
||||||
|
openBaoPath: kv/k8s/netbox/database
|
||||||
|
deletionPolicy: Retain
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## Append samples of your project ##
|
||||||
|
resources:
|
||||||
|
- database_v1alpha1_postgresqlinstance.yaml
|
||||||
|
- database_v1alpha1_postgresqltenant.yaml
|
||||||
|
# +kubebuilder:scaffold:manifestskustomizesamples
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# 初始架构
|
||||||
|
|
||||||
|
## 职责边界
|
||||||
|
|
||||||
|
Kubernetes API 保存期望状态和最近一次观察结果;PostgreSQL catalog 是 database、
|
||||||
|
role 和权限的事实来源;OpenBao 是凭据的事实来源。controller 不把明文密码写入
|
||||||
|
Kubernetes API、Event 或日志。
|
||||||
|
|
||||||
|
```text
|
||||||
|
Git / kubectl / Terraform / Backstage
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Kubernetes API (CRD)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
postgresql-tenant-operator
|
||||||
|
| |
|
||||||
|
v v
|
||||||
|
external PostgreSQL OpenBao
|
||||||
|
```
|
||||||
|
|
||||||
|
## 资源模型
|
||||||
|
|
||||||
|
### PostgreSQLInstance
|
||||||
|
|
||||||
|
Cluster-scoped,由平台管理员创建。它描述服务器端点、管理凭据的 OpenBao 引用,
|
||||||
|
以及租户可以申请的 extension 白名单。
|
||||||
|
|
||||||
|
### PostgreSQLTenant
|
||||||
|
|
||||||
|
Namespaced,描述一个应用租户,包括 database、无登录 owner role、应用 login
|
||||||
|
role、extensions、凭据路径和删除策略。
|
||||||
|
|
||||||
|
第一版规定一个 tenant 拥有一个 database 和一个 login role。跨租户 grant、多个
|
||||||
|
login role 和定时轮换等需求出现后再扩展 API。
|
||||||
|
|
||||||
|
## Reconcile 原则
|
||||||
|
|
||||||
|
- 每轮从 PostgreSQL 和 OpenBao 读取实际状态,不把 `status` 当作事实来源。
|
||||||
|
- 所有操作幂等;任意步骤失败后可以从下一轮继续。
|
||||||
|
- 先验证 extension 白名单,再执行任何变更。
|
||||||
|
- controller 生成密码,调用方只能得到 OpenBao 路径和状态。
|
||||||
|
- `metadata.generation` 只表示 spec 变更,不承载凭据版本语义。
|
||||||
|
|
||||||
|
## 删除
|
||||||
|
|
||||||
|
默认 `deletionPolicy: Retain`。删除 CR 时保留 PostgreSQL database、roles 和
|
||||||
|
OpenBao 数据。`Delete` 模式将在实现备份检查、活动连接处理和可测试的 finalizer
|
||||||
|
状态机后加入实际销毁逻辑。
|
||||||
|
|
||||||
|
## 暂不包含
|
||||||
|
|
||||||
|
- PostgreSQL 实例、VM 或存储的创建。
|
||||||
|
- Web UI 或独立 REST API;Kubernetes API 已提供 get、list、watch 和 RBAC。
|
||||||
|
- PostgreSQL 高可用和备份编排。
|
||||||
|
- 凭据轮换;controller 只负责首次生成长期凭据并写入 OpenBao。只有出现能够
|
||||||
|
重新加载凭据并妥善处理现有连接的实际消费者后,才重新评估轮换协议。
|
||||||
|
- 自动将 OpenBao 数据投射为 Kubernetes Secret;这由 External Secrets Operator
|
||||||
|
负责。
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
module git.ddupan.top/panxiao81/postgresql-tenant-operator
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.4
|
||||||
|
github.com/onsi/gomega v1.39.0
|
||||||
|
k8s.io/apimachinery v0.36.0
|
||||||
|
k8s.io/client-go v0.36.0
|
||||||
|
sigs.k8s.io/controller-runtime v0.24.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cel.dev/expr v0.25.1 // indirect
|
||||||
|
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||||
|
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0 // 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.20.2 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
|
github.com/google/cel-go v0.26.0 // indirect
|
||||||
|
github.com/google/gnostic-models v0.7.0 // indirect
|
||||||
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
|
github.com/spf13/cobra v1.10.2 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
|
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||||
|
golang.org/x/mod v0.32.0 // indirect
|
||||||
|
golang.org/x/net v0.49.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.34.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.33.0 // indirect
|
||||||
|
golang.org/x/time v0.14.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-20260128011058-8636f8732409 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||||
|
google.golang.org/grpc v1.79.3 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||||
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
k8s.io/api v0.36.0 // indirect
|
||||||
|
k8s.io/apiextensions-apiserver v0.36.0 // indirect
|
||||||
|
k8s.io/apiserver v0.36.0 // indirect
|
||||||
|
k8s.io/component-base v0.36.0 // indirect
|
||||||
|
k8s.io/klog/v2 v2.140.0 // indirect
|
||||||
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||||
|
k8s.io/streaming v0.36.0 // indirect
|
||||||
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
||||||
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect
|
||||||
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||||
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||||
|
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||||
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||||
|
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||||
|
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||||
|
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||||
|
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||||
|
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
|
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
|
||||||
|
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
|
||||||
|
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
|
||||||
|
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
|
||||||
|
github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
|
||||||
|
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
|
||||||
|
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.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||||
|
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.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||||
|
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||||
|
github.com/go-openapi/swag v0.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/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
|
||||||
|
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||||
|
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||||
|
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||||
|
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-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||||
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
|
||||||
|
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
|
||||||
|
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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||||
|
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||||
|
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
|
||||||
|
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
|
||||||
|
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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||||
|
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
|
||||||
|
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
|
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||||
|
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
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/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||||
|
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||||
|
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
|
||||||
|
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||||
|
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||||
|
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||||
|
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||||
|
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.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||||
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||||
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
|
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.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
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.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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
|
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-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/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.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||||
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/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.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.13.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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
|
||||||
|
k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
|
||||||
|
k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0=
|
||||||
|
k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug=
|
||||||
|
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
|
||||||
|
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
|
||||||
|
k8s.io/apiserver v0.36.0 h1:Jg5OFAENUACByUCg15CmhZAYrr5ZyJ+jodyA1mHl3YE=
|
||||||
|
k8s.io/apiserver v0.36.0/go.mod h1:mHvwdHf+qKEm+1/hYm756SV+oREOKSPnsjagOpx6Vho=
|
||||||
|
k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c=
|
||||||
|
k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y=
|
||||||
|
k8s.io/component-base v0.36.0 h1:hFjEktssxiJhrK1zfybkH4kJOi8iZuF+mIDCqS5+jRo=
|
||||||
|
k8s.io/component-base v0.36.0/go.mod h1:JZvIfcNHk+uck+8LhJzhSBtydWXaZNQwX2OdL+Mnwsk=
|
||||||
|
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||||
|
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
||||||
|
k8s.io/streaming v0.36.0 h1:agnTxU+NFulUrtYzXUGKO3ndEa8jKwht1Kwn9nu9x+4=
|
||||||
|
k8s.io/streaming v0.36.0/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s=
|
||||||
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
||||||
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||||
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec=
|
||||||
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||||
|
sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
|
||||||
|
sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
|
||||||
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||||
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||||
|
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||||
|
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||||
|
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||||
|
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
Copyright YEAR.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
databasev1alpha1 "git.ddupan.top/panxiao81/postgresql-tenant-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostgreSQLInstanceReconciler reconciles a PostgreSQLInstance object
|
||||||
|
type PostgreSQLInstanceReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=database.ddupan.top,resources=postgresqlinstances,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=database.ddupan.top,resources=postgresqlinstances/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=database.ddupan.top,resources=postgresqlinstances/finalizers,verbs=update
|
||||||
|
|
||||||
|
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||||
|
// move the current state of the cluster closer to the desired state.
|
||||||
|
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||||
|
// the PostgreSQLInstance object against the actual cluster state, and then
|
||||||
|
// perform operations to make the cluster state reflect the state specified by
|
||||||
|
// the user.
|
||||||
|
//
|
||||||
|
// For more details, check Reconcile and its Result here:
|
||||||
|
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
|
||||||
|
func (r *PostgreSQLInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
_ = logf.FromContext(ctx)
|
||||||
|
|
||||||
|
// TODO(user): your logic here
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *PostgreSQLInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&databasev1alpha1.PostgreSQLInstance{}).
|
||||||
|
Named("postgresqlinstance").
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
databasev1alpha1 "git.ddupan.top/panxiao81/postgresql-tenant-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("PostgreSQLInstance Controller", func() {
|
||||||
|
Context("When reconciling a resource", func() {
|
||||||
|
const (
|
||||||
|
resourceName = "test-resource"
|
||||||
|
resourceNamespace = "default"
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
typeNamespacedName := types.NamespacedName{
|
||||||
|
Name: resourceName,
|
||||||
|
Namespace: resourceNamespace,
|
||||||
|
}
|
||||||
|
postgresqlinstance := &databasev1alpha1.PostgreSQLInstance{}
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
By("creating the custom resource for the Kind PostgreSQLInstance")
|
||||||
|
err := k8sClient.Get(ctx, typeNamespacedName, postgresqlinstance)
|
||||||
|
if err != nil && errors.IsNotFound(err) {
|
||||||
|
resource := &databasev1alpha1.PostgreSQLInstance{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: resourceName,
|
||||||
|
Namespace: resourceNamespace,
|
||||||
|
},
|
||||||
|
// 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 := &databasev1alpha1.PostgreSQLInstance{}
|
||||||
|
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("Cleanup the specific resource instance PostgreSQLInstance")
|
||||||
|
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||||
|
})
|
||||||
|
It("should successfully reconcile the resource", func() {
|
||||||
|
By("Reconciling the created resource")
|
||||||
|
controllerReconciler := &PostgreSQLInstanceReconciler{
|
||||||
|
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.
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
databasev1alpha1 "git.ddupan.top/panxiao81/postgresql-tenant-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostgreSQLTenantReconciler reconciles a PostgreSQLTenant object
|
||||||
|
type PostgreSQLTenantReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=database.ddupan.top,resources=postgresqltenants,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=database.ddupan.top,resources=postgresqltenants/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=database.ddupan.top,resources=postgresqltenants/finalizers,verbs=update
|
||||||
|
|
||||||
|
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||||
|
// move the current state of the cluster closer to the desired state.
|
||||||
|
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||||
|
// the PostgreSQLTenant object against the actual cluster state, and then
|
||||||
|
// perform operations to make the cluster state reflect the state specified by
|
||||||
|
// the user.
|
||||||
|
//
|
||||||
|
// For more details, check Reconcile and its Result here:
|
||||||
|
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
|
||||||
|
func (r *PostgreSQLTenantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
_ = logf.FromContext(ctx)
|
||||||
|
|
||||||
|
// TODO(user): your logic here
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *PostgreSQLTenantReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&databasev1alpha1.PostgreSQLTenant{}).
|
||||||
|
Named("postgresqltenant").
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
databasev1alpha1 "git.ddupan.top/panxiao81/postgresql-tenant-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("PostgreSQLTenant Controller", func() {
|
||||||
|
Context("When reconciling a resource", func() {
|
||||||
|
const (
|
||||||
|
resourceName = "test-resource"
|
||||||
|
resourceNamespace = "default"
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
typeNamespacedName := types.NamespacedName{
|
||||||
|
Name: resourceName,
|
||||||
|
Namespace: resourceNamespace,
|
||||||
|
}
|
||||||
|
postgresqltenant := &databasev1alpha1.PostgreSQLTenant{}
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
By("creating the custom resource for the Kind PostgreSQLTenant")
|
||||||
|
err := k8sClient.Get(ctx, typeNamespacedName, postgresqltenant)
|
||||||
|
if err != nil && errors.IsNotFound(err) {
|
||||||
|
resource := &databasev1alpha1.PostgreSQLTenant{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: resourceName,
|
||||||
|
Namespace: resourceNamespace,
|
||||||
|
},
|
||||||
|
// 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 := &databasev1alpha1.PostgreSQLTenant{}
|
||||||
|
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("Cleanup the specific resource instance PostgreSQLTenant")
|
||||||
|
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||||
|
})
|
||||||
|
It("should successfully reconcile the resource", func() {
|
||||||
|
By("Reconciling the created resource")
|
||||||
|
controllerReconciler := &PostgreSQLTenantReconciler{
|
||||||
|
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.
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
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"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
. "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"
|
||||||
|
|
||||||
|
databasev1alpha1 "git.ddupan.top/panxiao81/postgresql-tenant-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 (
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
testEnv *envtest.Environment
|
||||||
|
cfg *rest.Config
|
||||||
|
k8sClient client.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestControllers(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
|
||||||
|
RunSpecs(t, "Controller Suite")
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = BeforeSuite(func() {
|
||||||
|
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
|
||||||
|
|
||||||
|
ctx, cancel = context.WithCancel(context.TODO())
|
||||||
|
|
||||||
|
var err error
|
||||||
|
err = databasev1alpha1.AddToScheme(scheme.Scheme)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// +kubebuilder:scaffold:scheme
|
||||||
|
|
||||||
|
By("bootstrapping test environment")
|
||||||
|
testEnv = &envtest.Environment{
|
||||||
|
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
|
||||||
|
ErrorIfCRDPathMissing: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the first found binary directory to allow running tests from IDEs
|
||||||
|
if getFirstFoundEnvTestBinaryDir() != "" {
|
||||||
|
testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// cfg is defined in this file globally.
|
||||||
|
cfg, err = testEnv.Start()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(cfg).NotTo(BeNil())
|
||||||
|
|
||||||
|
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(k8sClient).NotTo(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
var _ = AfterSuite(func() {
|
||||||
|
By("tearing down the test environment")
|
||||||
|
cancel()
|
||||||
|
Eventually(func() error {
|
||||||
|
return testEnv.Stop()
|
||||||
|
}, time.Minute, time.Second).Should(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
|
||||||
|
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
|
||||||
|
// controller-runtime. When running tests directly (e.g., via an IDE) without using
|
||||||
|
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
|
||||||
|
//
|
||||||
|
// This function streamlines the process by finding the required binaries, similar to
|
||||||
|
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
|
||||||
|
// properly set up, run 'make setup-envtest' beforehand.
|
||||||
|
func getFirstFoundEnvTestBinaryDir() string {
|
||||||
|
basePath := filepath.Join("..", "..", "bin", "k8s")
|
||||||
|
entries, err := os.ReadDir(basePath)
|
||||||
|
if err != nil {
|
||||||
|
logf.Log.Error(err, "Failed to read directory", "path", basePath)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
return filepath.Join(basePath, entry.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
//go:build e2e
|
||||||
|
// +build e2e
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/postgresql-tenant-operator/test/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// managerImage is the manager image to be built and loaded for testing.
|
||||||
|
managerImage = "example.com/postgresql-tenant-operator:v0.0.1"
|
||||||
|
// shouldCleanupCertManager tracks whether CertManager was installed by this suite.
|
||||||
|
shouldCleanupCertManager = false
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestE2E runs the e2e test suite to validate the solution in an isolated environment.
|
||||||
|
// The default setup requires Kind and CertManager.
|
||||||
|
//
|
||||||
|
// To enable kubectl kuberc (use custom kubectl configurations), set: KUBECTL_KUBERC=true
|
||||||
|
// By default, kuberc is disabled to ensure consistent test behavior across different environments.
|
||||||
|
// To skip CertManager installation, set: CERT_MANAGER_INSTALL_SKIP=true
|
||||||
|
func TestE2E(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "Starting postgresql-tenant-operator e2e test suite\n")
|
||||||
|
RunSpecs(t, "e2e suite")
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = BeforeSuite(func() {
|
||||||
|
By("building the manager image")
|
||||||
|
cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", managerImage))
|
||||||
|
_, err := utils.Run(cmd)
|
||||||
|
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager image")
|
||||||
|
|
||||||
|
// TODO(user): If you want to change the e2e test vendor from Kind,
|
||||||
|
// ensure the image is built and available, then remove the following block.
|
||||||
|
By("loading the manager image on Kind")
|
||||||
|
err = utils.LoadImageToKindClusterWithName(managerImage)
|
||||||
|
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager image into Kind")
|
||||||
|
|
||||||
|
configureKubectlKubeRC()
|
||||||
|
setupCertManager()
|
||||||
|
})
|
||||||
|
|
||||||
|
var _ = AfterSuite(func() {
|
||||||
|
teardownCertManager()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Disable kubectl kuberc by default for test isolation.
|
||||||
|
// This prevents local kubectl configurations from affecting test behavior.
|
||||||
|
// To enable kuberc, set: KUBECTL_KUBERC=true
|
||||||
|
func configureKubectlKubeRC() {
|
||||||
|
if os.Getenv("KUBECTL_KUBERC") != "true" {
|
||||||
|
By("disabling kubectl kuberc for test isolation")
|
||||||
|
err := os.Setenv("KUBECTL_KUBERC", "false")
|
||||||
|
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to disable kubectl kuberc")
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter,
|
||||||
|
"kubectl kuberc disabled for consistent test behavior (override with KUBECTL_KUBERC=true)\n")
|
||||||
|
} else {
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "kubectl kuberc enabled (KUBECTL_KUBERC=true)\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupCertManager installs CertManager if needed for webhook tests.
|
||||||
|
// Skips installation if CERT_MANAGER_INSTALL_SKIP=true or if already present.
|
||||||
|
func setupCertManager() {
|
||||||
|
if os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" {
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager installation (CERT_MANAGER_INSTALL_SKIP=true)\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
By("checking if CertManager is already installed")
|
||||||
|
if utils.IsCertManagerCRDsInstalled() {
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "CertManager is already installed. Skipping installation.\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark for cleanup before installation to handle interruptions and partial installs.
|
||||||
|
shouldCleanupCertManager = true
|
||||||
|
|
||||||
|
By("installing CertManager")
|
||||||
|
Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager")
|
||||||
|
}
|
||||||
|
|
||||||
|
// teardownCertManager uninstalls CertManager if it was installed by setupCertManager.
|
||||||
|
// This ensures we only remove what we installed.
|
||||||
|
func teardownCertManager() {
|
||||||
|
if !shouldCleanupCertManager {
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager cleanup (not installed by this suite)\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
By("uninstalling CertManager")
|
||||||
|
utils.UninstallCertManager()
|
||||||
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
//go:build e2e
|
||||||
|
// +build e2e
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/postgresql-tenant-operator/test/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// namespace where the project is deployed in
|
||||||
|
const namespace = "postgresql-tenant-operator-system"
|
||||||
|
|
||||||
|
// serviceAccountName created for the project
|
||||||
|
const serviceAccountName = "postgresql-tenant-operator-controller-manager"
|
||||||
|
|
||||||
|
// metricsServiceName is the name of the metrics service of the project
|
||||||
|
const metricsServiceName = "postgresql-tenant-operator-controller-manager-metrics-service"
|
||||||
|
|
||||||
|
// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data
|
||||||
|
const metricsRoleBindingName = "postgresql-tenant-operator-metrics-binding"
|
||||||
|
|
||||||
|
var _ = Describe("Manager", Ordered, func() {
|
||||||
|
var controllerPodName string
|
||||||
|
|
||||||
|
// Before running the tests, set up the environment by creating the namespace,
|
||||||
|
// enforce the restricted security policy to the namespace, installing CRDs,
|
||||||
|
// and deploying the controller.
|
||||||
|
BeforeAll(func() {
|
||||||
|
By("creating manager namespace")
|
||||||
|
cmd := exec.Command("kubectl", "create", "ns", namespace)
|
||||||
|
_, err := utils.Run(cmd)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "Failed to create namespace")
|
||||||
|
|
||||||
|
By("labeling the namespace to enforce the restricted security policy")
|
||||||
|
cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace,
|
||||||
|
"pod-security.kubernetes.io/enforce=restricted")
|
||||||
|
_, err = utils.Run(cmd)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy")
|
||||||
|
|
||||||
|
By("installing CRDs")
|
||||||
|
cmd = exec.Command("make", "install")
|
||||||
|
_, err = utils.Run(cmd)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs")
|
||||||
|
|
||||||
|
By("deploying the controller-manager")
|
||||||
|
cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", managerImage))
|
||||||
|
_, err = utils.Run(cmd)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager")
|
||||||
|
})
|
||||||
|
|
||||||
|
// After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs,
|
||||||
|
// and deleting the namespace.
|
||||||
|
AfterAll(func() {
|
||||||
|
By("cleaning up the curl pod for metrics")
|
||||||
|
cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace)
|
||||||
|
_, _ = utils.Run(cmd)
|
||||||
|
|
||||||
|
By("undeploying the controller-manager")
|
||||||
|
cmd = exec.Command("make", "undeploy")
|
||||||
|
_, _ = utils.Run(cmd)
|
||||||
|
|
||||||
|
By("uninstalling CRDs")
|
||||||
|
cmd = exec.Command("make", "uninstall")
|
||||||
|
_, _ = utils.Run(cmd)
|
||||||
|
|
||||||
|
By("removing manager namespace")
|
||||||
|
cmd = exec.Command("kubectl", "delete", "ns", namespace)
|
||||||
|
_, _ = utils.Run(cmd)
|
||||||
|
})
|
||||||
|
|
||||||
|
// After each test, check for failures and collect logs, events,
|
||||||
|
// and pod descriptions for debugging.
|
||||||
|
AfterEach(func() {
|
||||||
|
specReport := CurrentSpecReport()
|
||||||
|
if specReport.Failed() {
|
||||||
|
By("Fetching controller manager pod logs")
|
||||||
|
cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
|
||||||
|
controllerLogs, err := utils.Run(cmd)
|
||||||
|
if err == nil {
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs)
|
||||||
|
} else {
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
By("Fetching Kubernetes events")
|
||||||
|
cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp")
|
||||||
|
eventsOutput, err := utils.Run(cmd)
|
||||||
|
if err == nil {
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput)
|
||||||
|
} else {
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
By("Fetching curl-metrics logs")
|
||||||
|
cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
|
||||||
|
metricsOutput, err := utils.Run(cmd)
|
||||||
|
if err == nil {
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput)
|
||||||
|
} else {
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
By("Fetching controller manager pod description")
|
||||||
|
cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace)
|
||||||
|
podDescription, err := utils.Run(cmd)
|
||||||
|
if err == nil {
|
||||||
|
fmt.Println("Pod description:\n", podDescription)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Failed to describe controller pod")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
SetDefaultEventuallyTimeout(2 * time.Minute)
|
||||||
|
SetDefaultEventuallyPollingInterval(time.Second)
|
||||||
|
|
||||||
|
Context("Manager", func() {
|
||||||
|
It("should run successfully", func() {
|
||||||
|
By("validating that the controller-manager pod is running as expected")
|
||||||
|
verifyControllerUp := func(g Gomega) {
|
||||||
|
By("getting the name of the controller-manager pod")
|
||||||
|
cmd := exec.Command("kubectl", "get",
|
||||||
|
"pods", "-l", "control-plane=controller-manager",
|
||||||
|
"-o", "go-template={{ range .items }}"+
|
||||||
|
"{{ if not .metadata.deletionTimestamp }}"+
|
||||||
|
"{{ .metadata.name }}"+
|
||||||
|
"{{ \"\\n\" }}{{ end }}{{ end }}",
|
||||||
|
"-n", namespace,
|
||||||
|
)
|
||||||
|
|
||||||
|
podOutput, err := utils.Run(cmd)
|
||||||
|
g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information")
|
||||||
|
podNames := utils.GetNonEmptyLines(podOutput)
|
||||||
|
g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running")
|
||||||
|
controllerPodName = podNames[0]
|
||||||
|
g.Expect(controllerPodName).To(ContainSubstring("controller-manager"))
|
||||||
|
|
||||||
|
By("validating the pod's status")
|
||||||
|
cmd = exec.Command("kubectl", "get",
|
||||||
|
"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
|
||||||
|
"-n", namespace,
|
||||||
|
)
|
||||||
|
output, err := utils.Run(cmd)
|
||||||
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status")
|
||||||
|
}
|
||||||
|
Eventually(verifyControllerUp).Should(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should ensure the metrics endpoint is serving metrics", func() {
|
||||||
|
By("creating a ClusterRoleBinding for the service account to allow access to metrics")
|
||||||
|
cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName,
|
||||||
|
"--clusterrole=postgresql-tenant-operator-metrics-reader",
|
||||||
|
fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName),
|
||||||
|
)
|
||||||
|
_, err := utils.Run(cmd)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding")
|
||||||
|
|
||||||
|
By("validating that the metrics service is available")
|
||||||
|
cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
|
||||||
|
_, err = utils.Run(cmd)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "Metrics service should exist")
|
||||||
|
|
||||||
|
By("getting the service account token")
|
||||||
|
token, err := serviceAccountToken()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(token).NotTo(BeEmpty())
|
||||||
|
|
||||||
|
By("ensuring the controller pod is ready")
|
||||||
|
verifyControllerPodReady := func(g Gomega) {
|
||||||
|
cmd := exec.Command("kubectl", "get", "pod", controllerPodName, "-n", namespace,
|
||||||
|
"-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}")
|
||||||
|
output, err := utils.Run(cmd)
|
||||||
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
g.Expect(output).To(Equal("True"), "Controller pod not ready")
|
||||||
|
}
|
||||||
|
Eventually(verifyControllerPodReady, 3*time.Minute, time.Second).Should(Succeed())
|
||||||
|
|
||||||
|
By("verifying that the controller manager is serving the metrics server")
|
||||||
|
verifyMetricsServerStarted := func(g Gomega) {
|
||||||
|
cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
|
||||||
|
output, err := utils.Run(cmd)
|
||||||
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
g.Expect(output).To(ContainSubstring("Serving metrics server"),
|
||||||
|
"Metrics server not yet started")
|
||||||
|
}
|
||||||
|
Eventually(verifyMetricsServerStarted, 3*time.Minute, time.Second).Should(Succeed())
|
||||||
|
|
||||||
|
// +kubebuilder:scaffold:e2e-metrics-webhooks-readiness
|
||||||
|
|
||||||
|
By("creating the curl-metrics pod to access the metrics endpoint")
|
||||||
|
cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never",
|
||||||
|
"--namespace", namespace,
|
||||||
|
"--image=curlimages/curl:latest",
|
||||||
|
"--overrides",
|
||||||
|
fmt.Sprintf(`{
|
||||||
|
"spec": {
|
||||||
|
"containers": [{
|
||||||
|
"name": "curl",
|
||||||
|
"image": "curlimages/curl:latest",
|
||||||
|
"command": ["/bin/sh", "-c"],
|
||||||
|
"args": [
|
||||||
|
"for i in $(seq 1 30); do curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics && exit 0 || sleep 2; done; exit 1"
|
||||||
|
],
|
||||||
|
"securityContext": {
|
||||||
|
"readOnlyRootFilesystem": true,
|
||||||
|
"allowPrivilegeEscalation": false,
|
||||||
|
"capabilities": {
|
||||||
|
"drop": ["ALL"]
|
||||||
|
},
|
||||||
|
"runAsNonRoot": true,
|
||||||
|
"runAsUser": 1000,
|
||||||
|
"seccompProfile": {
|
||||||
|
"type": "RuntimeDefault"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"serviceAccountName": "%s"
|
||||||
|
}
|
||||||
|
}`, token, metricsServiceName, namespace, serviceAccountName))
|
||||||
|
_, err = utils.Run(cmd)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod")
|
||||||
|
|
||||||
|
By("waiting for the curl-metrics pod to complete.")
|
||||||
|
verifyCurlUp := func(g Gomega) {
|
||||||
|
cmd := exec.Command("kubectl", "get", "pods", "curl-metrics",
|
||||||
|
"-o", "jsonpath={.status.phase}",
|
||||||
|
"-n", namespace)
|
||||||
|
output, err := utils.Run(cmd)
|
||||||
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status")
|
||||||
|
}
|
||||||
|
Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed())
|
||||||
|
|
||||||
|
By("getting the metrics by checking curl-metrics logs")
|
||||||
|
verifyMetricsAvailable := func(g Gomega) {
|
||||||
|
metricsOutput, err := getMetricsOutput()
|
||||||
|
g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
|
||||||
|
g.Expect(metricsOutput).NotTo(BeEmpty())
|
||||||
|
g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK"))
|
||||||
|
}
|
||||||
|
Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
// +kubebuilder:scaffold:e2e-webhooks-checks
|
||||||
|
|
||||||
|
// TODO: Customize the e2e test suite with scenarios specific to your project.
|
||||||
|
// Consider applying sample/CR(s) and check their status and/or verifying
|
||||||
|
// the reconciliation by using the metrics, i.e.:
|
||||||
|
// metricsOutput, err := getMetricsOutput()
|
||||||
|
// Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
|
||||||
|
// Expect(metricsOutput).To(ContainSubstring(
|
||||||
|
// fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
|
||||||
|
// strings.ToLower(<Kind>),
|
||||||
|
// ))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// serviceAccountToken returns a token for the specified service account in the given namespace.
|
||||||
|
// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
|
||||||
|
// and parsing the resulting token from the API response.
|
||||||
|
func serviceAccountToken() (string, error) {
|
||||||
|
const tokenRequestRawString = `{
|
||||||
|
"apiVersion": "authentication.k8s.io/v1",
|
||||||
|
"kind": "TokenRequest"
|
||||||
|
}`
|
||||||
|
|
||||||
|
By("creating temporary file to store the token request")
|
||||||
|
secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
|
||||||
|
tokenRequestFile := filepath.Join("/tmp", secretName)
|
||||||
|
err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out string
|
||||||
|
verifyTokenCreation := func(g Gomega) {
|
||||||
|
By("executing kubectl command to create the token")
|
||||||
|
cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf(
|
||||||
|
"/api/v1/namespaces/%s/serviceaccounts/%s/token",
|
||||||
|
namespace,
|
||||||
|
serviceAccountName,
|
||||||
|
), "-f", tokenRequestFile)
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("parsing the JSON output to extract the token")
|
||||||
|
var token tokenRequest
|
||||||
|
err = json.Unmarshal(output, &token)
|
||||||
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
out = token.Status.Token
|
||||||
|
}
|
||||||
|
Eventually(verifyTokenCreation).Should(Succeed())
|
||||||
|
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
|
||||||
|
func getMetricsOutput() (string, error) {
|
||||||
|
By("getting the curl-metrics logs")
|
||||||
|
cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
|
||||||
|
return utils.Run(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
|
||||||
|
// containing only the token field that we need to extract.
|
||||||
|
type tokenRequest struct {
|
||||||
|
Status struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
} `json:"status"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
certmanagerVersion = "v1.20.2"
|
||||||
|
certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml"
|
||||||
|
|
||||||
|
defaultKindBinary = "kind"
|
||||||
|
defaultKindCluster = "kind"
|
||||||
|
)
|
||||||
|
|
||||||
|
func warnError(err error) {
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the provided command within this context
|
||||||
|
func Run(cmd *exec.Cmd) (string, error) {
|
||||||
|
dir, _ := GetProjectDir()
|
||||||
|
cmd.Dir = dir
|
||||||
|
|
||||||
|
if err := os.Chdir(cmd.Dir); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Env = append(os.Environ(), "GO111MODULE=on")
|
||||||
|
command := strings.Join(cmd.Args, " ")
|
||||||
|
_, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UninstallCertManager uninstalls the cert manager
|
||||||
|
func UninstallCertManager() {
|
||||||
|
url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
|
||||||
|
cmd := exec.Command("kubectl", "delete", "-f", url)
|
||||||
|
if _, err := Run(cmd); err != nil {
|
||||||
|
warnError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete leftover leases in kube-system (not cleaned by default)
|
||||||
|
kubeSystemLeases := []string{
|
||||||
|
"cert-manager-cainjector-leader-election",
|
||||||
|
"cert-manager-controller",
|
||||||
|
}
|
||||||
|
for _, lease := range kubeSystemLeases {
|
||||||
|
cmd = exec.Command("kubectl", "delete", "lease", lease,
|
||||||
|
"-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0")
|
||||||
|
if _, err := Run(cmd); err != nil {
|
||||||
|
warnError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallCertManager installs the cert manager bundle.
|
||||||
|
func InstallCertManager() error {
|
||||||
|
url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
|
||||||
|
cmd := exec.Command("kubectl", "apply", "-f", url)
|
||||||
|
if _, err := Run(cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Wait for cert-manager-webhook to be ready, which can take time if cert-manager
|
||||||
|
// was re-installed after uninstalling on a cluster.
|
||||||
|
cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook",
|
||||||
|
"--for", "condition=Available",
|
||||||
|
"--namespace", "cert-manager",
|
||||||
|
"--timeout", "5m",
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := Run(cmd)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed
|
||||||
|
// by verifying the existence of key CRDs related to Cert Manager.
|
||||||
|
func IsCertManagerCRDsInstalled() bool {
|
||||||
|
// List of common Cert Manager CRDs
|
||||||
|
certManagerCRDs := []string{
|
||||||
|
"certificates.cert-manager.io",
|
||||||
|
"issuers.cert-manager.io",
|
||||||
|
"clusterissuers.cert-manager.io",
|
||||||
|
"certificaterequests.cert-manager.io",
|
||||||
|
"orders.acme.cert-manager.io",
|
||||||
|
"challenges.acme.cert-manager.io",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the kubectl command to get all CRDs
|
||||||
|
cmd := exec.Command("kubectl", "get", "crds")
|
||||||
|
output, err := Run(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any of the Cert Manager CRDs are present
|
||||||
|
crdList := GetNonEmptyLines(output)
|
||||||
|
for _, crd := range certManagerCRDs {
|
||||||
|
for _, line := range crdList {
|
||||||
|
if strings.Contains(line, crd) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadImageToKindClusterWithName loads a local docker image to the kind cluster
|
||||||
|
func LoadImageToKindClusterWithName(name string) error {
|
||||||
|
cluster := defaultKindCluster
|
||||||
|
if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
|
||||||
|
cluster = v
|
||||||
|
}
|
||||||
|
kindOptions := []string{"load", "docker-image", name, "--name", cluster}
|
||||||
|
kindBinary := defaultKindBinary
|
||||||
|
if v, ok := os.LookupEnv("KIND"); ok {
|
||||||
|
kindBinary = v
|
||||||
|
}
|
||||||
|
cmd := exec.Command(kindBinary, kindOptions...)
|
||||||
|
_, err := Run(cmd)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNonEmptyLines converts given command output string into individual objects
|
||||||
|
// according to line breakers, and ignores the empty elements in it.
|
||||||
|
func GetNonEmptyLines(output string) []string {
|
||||||
|
var res []string
|
||||||
|
elements := strings.SplitSeq(output, "\n")
|
||||||
|
for element := range elements {
|
||||||
|
if element != "" {
|
||||||
|
res = append(res, element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectDir will return the directory where the project is
|
||||||
|
func GetProjectDir() (string, error) {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return wd, fmt.Errorf("failed to get current working directory: %w", err)
|
||||||
|
}
|
||||||
|
wd = strings.ReplaceAll(wd, "/test/e2e", "")
|
||||||
|
return wd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UncommentCode searches for target in the file and remove the comment prefix
|
||||||
|
// of the target content. The target content may span multiple lines.
|
||||||
|
func UncommentCode(filename, target, prefix string) error {
|
||||||
|
// false positive
|
||||||
|
// nolint:gosec
|
||||||
|
content, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read file %q: %w", filename, err)
|
||||||
|
}
|
||||||
|
strContent := string(content)
|
||||||
|
|
||||||
|
idx := strings.Index(strContent, target)
|
||||||
|
if idx < 0 {
|
||||||
|
return fmt.Errorf("unable to find the code %q to be uncommented", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := new(bytes.Buffer)
|
||||||
|
_, err = out.Write(content[:idx])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write to output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(bytes.NewBufferString(target))
|
||||||
|
if !scanner.Scan() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil {
|
||||||
|
return fmt.Errorf("failed to write to output: %w", err)
|
||||||
|
}
|
||||||
|
// Avoid writing a newline in case the previous line was the last in target.
|
||||||
|
if !scanner.Scan() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, err = out.WriteString("\n"); err != nil {
|
||||||
|
return fmt.Errorf("failed to write to output: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = out.Write(content[idx+len(target):]); err != nil {
|
||||||
|
return fmt.Errorf("failed to write to output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// false positive
|
||||||
|
// nolint:gosec
|
||||||
|
if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write file %q: %w", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user