Compare commits

...

20 commits
v0.2.0 ... main

Author SHA1 Message Date
b4347cc8a2
chore: update migrations & images and switch to new registry
Some checks failed
Lint / Run on Ubuntu (push) Has been cancelled
E2E Tests / Run on Ubuntu (push) Has been cancelled
Tests / Run on Ubuntu (push) Has been cancelled
Postgres image / build (arm64, 15) (push) Successful in 17s
Postgres image / build (arm64, 17) (push) Successful in 17s
Docs / deploy (push) Successful in 1m30s
Postgres image / build (amd64, 15) (push) Successful in 16s
Postgres image / build (amd64, 17) (push) Successful in 16s
Postgres image / manifest (15) (push) Successful in 17s
Postgres image / manifest (17) (push) Successful in 17s
release / release (push) Successful in 5m12s
2025-04-01 20:27:03 +02:00
87a06dac66
feat(ci): update Postgres minor version and delete temporary tags
Some checks failed
Tests / Run on Ubuntu (push) Has been cancelled
E2E Tests / Run on Ubuntu (push) Has been cancelled
release / release (push) Has been cancelled
Lint / Run on Ubuntu (push) Has been cancelled
Docs / deploy (push) Has been cancelled
Postgres image / build (arm64, 15) (push) Successful in 18s
Postgres image / build (arm64, 17) (push) Successful in 17s
Postgres image / build (amd64, 15) (push) Successful in 23s
Postgres image / build (amd64, 17) (push) Successful in 18s
Postgres image / manifest (15) (push) Successful in 17s
Postgres image / manifest (17) (push) Successful in 18s
2025-03-26 15:42:57 +01:00
366ceece24
ci: switch to Harbor registry
Some checks failed
Docs / deploy (push) Successful in 48s
Lint / Run on Ubuntu (push) Failing after 1m22s
release / release (push) Has been cancelled
E2E Tests / Run on Ubuntu (push) Has been cancelled
Tests / Run on Ubuntu (push) Has been cancelled
Postgres image / build (amd64, 15) (push) Successful in 21s
Postgres image / build (amd64, 17) (push) Successful in 18s
Postgres image / build (arm64, 17) (push) Successful in 17s
Postgres image / build (arm64, 15) (push) Successful in 44m5s
Postgres image / manifest (15) (push) Successful in 8s
Postgres image / manifest (17) (push) Successful in 7s
2025-03-24 21:36:14 +01:00
8b2425d16d
refactor: don't mount service account token into workloads
Some checks failed
Lint / Run on Ubuntu (push) Failing after 41s
Docs / deploy (push) Successful in 44s
release / release (push) Successful in 4m34s
Tests / Run on Ubuntu (push) Failing after 32s
E2E Tests / Run on Ubuntu (push) Failing after 5m18s
closes #9
2025-02-13 20:17:18 +01:00
101bf971a7
chore: update to Go 1.24
Some checks failed
Docs / deploy (push) Successful in 37s
Lint / Run on Ubuntu (push) Failing after 1m3s
E2E Tests / Run on Ubuntu (push) Failing after 5m18s
release / release (push) Successful in 5m23s
Tests / Run on Ubuntu (push) Failing after 50s
2025-02-13 19:49:07 +01:00
c4a43b82d3
fix(ci): remove cache for postgres images for now
Some checks failed
Lint / Run on Ubuntu (push) Failing after 22s
Postgres image / build (arm64, 15) (push) Successful in 19s
Tests / Run on Ubuntu (push) Failing after 2m19s
Docs / deploy (push) Successful in 46s
Postgres image / build (amd64, 15) (push) Successful in 24s
Postgres image / build (amd64, 17) (push) Successful in 23s
Postgres image / manifest (17) (push) Successful in 7s
Postgres image / build (arm64, 17) (push) Successful in 18s
E2E Tests / Run on Ubuntu (push) Failing after 3m22s
Postgres image / manifest (15) (push) Successful in 10s
release / release (push) Successful in 2m1s
2025-02-13 16:19:56 +01:00
1f285f6492
chore(postgres): schedule new image every week
Some checks failed
Docs / deploy (push) Successful in 43s
Lint / Run on Ubuntu (push) Failing after 1m30s
Postgres image / manifest (17) (push) Has been skipped
E2E Tests / Run on Ubuntu (push) Failing after 3m18s
release / release (push) Failing after 3m23s
Tests / Run on Ubuntu (push) Failing after 3m8s
Postgres image / build (arm64, 15) (push) Failing after 24s
Postgres image / build (arm64, 17) (push) Failing after 21s
Postgres image / build (amd64, 15) (push) Failing after 12m35s
Postgres image / build (amd64, 17) (push) Failing after 16m31s
Postgres image / manifest (15) (push) Has been cancelled
2025-02-13 13:08:57 +01:00
2d71a4a132
fix(db): track state of migrations and execute them again when necessary
Some checks failed
Docs / deploy (push) Successful in 1m38s
Lint / Run on Ubuntu (push) Failing after 4m11s
release / release (push) Successful in 4m56s
E2E Tests / Run on Ubuntu (push) Failing after 4m54s
Tests / Run on Ubuntu (push) Failing after 5m0s
2025-02-13 09:29:47 +01:00
264b30e8a2
feat(ci): configure image build caching
Some checks failed
Docs / deploy (push) Has been cancelled
Lint / Run on Ubuntu (push) Has been cancelled
release / release (push) Has been cancelled
E2E Tests / Run on Ubuntu (push) Has been cancelled
Tests / Run on Ubuntu (push) Has been cancelled
2025-02-12 20:04:02 +01:00
7f56a3db56
feat: custom postgres images
Some checks failed
Lint / Run on Ubuntu (push) Failing after 4m51s
Docs / deploy (push) Successful in 1m59s
release / release (push) Successful in 5m15s
E2E Tests / Run on Ubuntu (push) Failing after 3m1s
Tests / Run on Ubuntu (push) Failing after 3m2s
Postgres image / build (amd64, 15) (push) Successful in 24s
Postgres image / build (amd64, 17) (push) Successful in 20s
Postgres image / build (arm64, 15) (push) Successful in 41m50s
Postgres image / build (arm64, 17) (push) Successful in 42m14s
Postgres image / manifest (17) (push) Successful in 6s
Postgres image / manifest (15) (push) Successful in 7s
2025-02-12 17:17:43 +01:00
9d02a2d90b
feat(webhook): validate dashboard auth spec
Some checks failed
E2E Tests / Run on Ubuntu (push) Failing after 29s
Tests / Run on Ubuntu (push) Failing after 3m35s
Lint / Run on Ubuntu (push) Failing after 4m40s
Docs / deploy (push) Successful in 2m0s
release / release (push) Successful in 5m19s
- will check whether either OIDC issuer or oauth2 endpoints are set
- will check whether basic auth credentials are configured
2025-02-05 21:09:44 +01:00
3c13eb0d6b
feat(apigateay): add OIDC and basic auth support
Some checks failed
Docs / deploy (push) Successful in 2m6s
Lint / Run on Ubuntu (push) Successful in 5m7s
release / release (push) Successful in 5m38s
E2E Tests / Run on Ubuntu (push) Failing after 4m7s
Tests / Run on Ubuntu (push) Has been cancelled
- when setting an OIDC issuer URL the defaulter will fetch and set
  authorization and token endpoints
- basic auth allows to use either inline hashed credentials or plaintext
  credentials from a secret that are automatically hashed
- finish TLS support for API & dashboard listeners
2025-02-05 20:51:36 +01:00
e9302c51be
feat(apigateway): allow to enable debug logging
Some checks failed
Tests / Run on Ubuntu (push) Has been cancelled
E2E Tests / Run on Ubuntu (push) Has been cancelled
Lint / Run on Ubuntu (push) Has been cancelled
release / release (push) Successful in 5m45s
Docs / deploy (push) Failing after 12m55s
2025-02-04 16:42:17 +01:00
45630f7326
docs: update CRD docs
Some checks failed
Lint / Run on Ubuntu (push) Has been cancelled
Tests / Run on Ubuntu (push) Has been cancelled
E2E Tests / Run on Ubuntu (push) Has been cancelled
release / release (push) Has been cancelled
Docs / deploy (push) Successful in 2m1s
2025-02-03 16:09:47 +01:00
6c61adb1c7
fix: route propagation
Some checks failed
Docs / deploy (push) Successful in 1m53s
E2E Tests / Run on Ubuntu (push) Has been cancelled
Tests / Run on Ubuntu (push) Has been cancelled
Lint / Run on Ubuntu (push) Has been cancelled
release / release (push) Successful in 7m14s
2025-02-03 15:51:44 +01:00
8066c34eb5
chore: update deps 2025-02-03 15:37:20 +01:00
867daaa375
fix(envoy): version not handled properly 2025-02-03 15:16:54 +01:00
0fccef973f
feat(dashboard): PoC Oauth2 auth 2025-02-03 09:57:05 +01:00
89b682935b
docs: extend docs
Some checks failed
Docs / deploy (push) Successful in 1m56s
Lint / Run on Ubuntu (push) Failing after 4m27s
release / release (push) Successful in 5m46s
E2E Tests / Run on Ubuntu (push) Failing after 3m50s
Tests / Run on Ubuntu (push) Failing after 3m23s
2025-01-28 18:01:34 +01:00
c0cbd22bb0
refactor(apigateway): configure api & dashboard listeneres individually
Some checks failed
Lint / Run on Ubuntu (push) Has been cancelled
E2E Tests / Run on Ubuntu (push) Has been cancelled
Tests / Run on Ubuntu (push) Has been cancelled
Docs / deploy (push) Successful in 1m54s
release / release (push) Successful in 7m41s
2025-01-24 09:16:58 +01:00
164 changed files with 6935 additions and 6249 deletions

View file

@ -35,7 +35,8 @@ jobs:
run: mkdocs build run: mkdocs build
- name: Copy files to the s3 website content bucket - name: Copy files to the s3 website content bucket
if: ${{ startsWith(github.ref, 'refs/tags/v') }} # for the time being, let's just always deploy the docs
# if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: rclone sync site/ HCLOUD:/1661580-supabase-operator-docs/ run: rclone sync site/ HCLOUD:/1661580-supabase-operator-docs/
env: env:
RCLONE_CONFIG_HCLOUD_TYPE: s3 RCLONE_CONFIG_HCLOUD_TYPE: s3

View file

@ -28,4 +28,4 @@ jobs:
- name: Run linter - name: Run linter
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v6
with: with:
version: v1.61 version: v1.63.4

80
.github/workflows/postgres.yml vendored Normal file
View file

@ -0,0 +1,80 @@
name: Postgres image
on:
schedule:
# every Thursday 2:30am
- cron: "30 2 * * 2"
push:
paths:
- .github/workflows/postgres.yml
- postgres/**
branches:
- main
tags:
- "v*"
env:
MINOR_VERSIONS: '{"15":"12","17":"4"}'
jobs:
build:
strategy:
matrix:
arch:
- arm64
- amd64
postgres_major:
- "15"
- "17"
runs-on: ubuntu-latest-${{ matrix.arch }}
steps:
- uses: actions/checkout@v4
- name: Login to container registry
uses: docker/login-action@v3
with:
registry: registry.icb4dc0.de
username: ${{ secrets.HARBOR_USER }}
password: ${{ secrets.HARBOR_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
file: postgres/Dockerfile
push: true
tags: registry.icb4dc0.de/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}.${{ github.run_number }}-${{ matrix.arch }}
build-args: |
POSTGRES_MAJOR=${{ matrix.postgres_major }}
POSTGRES_MINOR=${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}
manifest:
strategy:
matrix:
postgres_major:
- "15"
- "17"
runs-on: ubuntu-latest
needs:
- build
steps:
- name: Login to container registry
uses: docker/login-action@v3
with:
registry: registry.icb4dc0.de
username: ${{ secrets.HARBOR_USER }}
password: ${{ secrets.HARBOR_TOKEN }}
- name: Install skopeo
run: |
apt-get update
apt-get install -y skopeo
- name: Create manifest
run: |
docker buildx imagetools create \
-t registry.icb4dc0.de/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }} \
-t registry.icb4dc0.de/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}.${{ github.run_number }} \
registry.icb4dc0.de/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}.${{ github.run_number }}-arm64 \
registry.icb4dc0.de/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}.${{ github.run_number }}-amd64
skopeo delete docker://registry.icb4dc0.de/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}.${{ github.run_number }}-arm64
skopeo delete docker://registry.icb4dc0.de/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}.${{ github.run_number }}-amd64

View file

@ -22,9 +22,9 @@ jobs:
- name: Login to container registry - name: Login to container registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: code.icb4dc0.de registry: registry.icb4dc0.de
username: prskr username: ${{ secrets.HARBOR_USER }}
password: ${{ secrets.RELEASE_TOKEN }} password: ${{ secrets.HARBOR_TOKEN }}
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
@ -36,7 +36,6 @@ jobs:
- name: Init go - name: Init go
run: | run: |
go mod download go mod download
go mod download -modfile tools/go.mod
- name: Snapshot release - name: Snapshot release
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6

View file

@ -43,3 +43,8 @@ jobs:
run: | run: |
go mod tidy go mod tidy
make test-e2e make test-e2e
- name: Cleanup kind cluster
if: always()
run: |
kind delete cluster

View file

@ -1,36 +1,18 @@
version: "2"
run: run:
timeout: 5m
allow-parallel-runners: true allow-parallel-runners: true
issues:
# don't skip warning about doc comments
# don't exclude the default set of lint
exclude-use-default: false
# restore some of the defaults
# (fill in the rest as needed)
exclude-rules:
- path: "api/*"
linters:
- lll
- path: "internal/*"
linters:
- dupl
- lll
linters: linters:
disable-all: true default: none
enable: enable:
- copyloopvar
- dupl - dupl
- errcheck - errcheck
- copyloopvar
- ginkgolinter - ginkgolinter
- goconst - goconst
- gocyclo - gocyclo
- gofmt
- goimports
- goheader
- gosimple
# enable when the TODOs are fixed # enable when the TODOs are fixed
# - godox # - godox
- goheader
- govet - govet
- ineffassign - ineffassign
- lll - lll
@ -39,43 +21,65 @@ linters:
- prealloc - prealloc
- revive - revive
- staticcheck - staticcheck
- typecheck
- unconvert - unconvert
- unparam - unparam
- unused - unused
settings:
linters-settings: goheader:
revive: values:
const:
AUTHOR: Peter Kurfer
template-path: hack/header.tmpl
importas:
alias:
- pkg: k8s.io/api/(\w+)/(v[\w\d]+)
alias: $1$2
- pkg: k8s.io/apimachinery/pkg/apis/meta/v1
alias: metav1
no-unaliased: true
no-extra-aliases: true
revive:
rules:
- name: comment-spacings
exclusions:
generated: lax
rules: rules:
- name: comment-spacings - linters:
gci: - lll
sections: path: api/*
- standard - linters:
- default - dupl
- prefix(code.icb4dc0.de/prskr/supabase-operator) - lll
- alias path: internal/*
- blank paths:
- dot - third_party$
goimports: - builtin$
local-prefixes: code.icb4dc0.de/prskr/supabase-operator - examples$
goheader:
values:
const:
AUTHOR: Peter Kurfer
template-path: hack/header.tmpl
importas:
no-unaliased: true
no-extra-aliases: true
alias:
- pkg: k8s.io/api/(\w+)/(v[\w\d]+)
alias: $1$2
- pkg: "k8s.io/apimachinery/pkg/apis/meta/v1"
alias: metav1
severity: severity:
default-severity: error default: error
rules: rules:
- linters: - linters:
- godox - godox
severity: info severity: info
formatters:
enable:
- gofmt
- goimports
settings:
gci:
sections:
- standard
- default
- prefix(code.icb4dc0.de/prskr/supabase-operator)
- alias
- blank
- dot
goimports:
local-prefixes:
- code.icb4dc0.de/prskr/supabase-operator
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View file

@ -8,7 +8,7 @@ before:
- go mod tidy - go mod tidy
- go run mage.go GenerateAll - go run mage.go GenerateAll
- mkdir -p out - mkdir -p out
- sh -c "cd config/release/default && kustomize edit set image supabase-operator=code.icb4dc0.de/prskr/supabase-operator:{{.Version}}" - sh -c "cd config/release/default && kustomize edit set image supabase-operator=registry.icb4dc0.de/supabase-operator/controller:{{.Version}}"
- sh -c "kustomize build config/release/default > out/operator_manifest.yaml" - sh -c "kustomize build config/release/default > out/operator_manifest.yaml"
builds: builds:
@ -28,7 +28,7 @@ kos:
base_image: gcr.io/distroless/static:nonroot base_image: gcr.io/distroless/static:nonroot
user: "65532:65532" user: "65532:65532"
repositories: repositories:
- code.icb4dc0.de/prskr/supabase-operator - registry.icb4dc0.de/supabase-operator/controller
platforms: platforms:
- linux/amd64 - linux/amd64
- linux/arm64 - linux/arm64

View file

@ -2,8 +2,7 @@
# git hook pre commit # git hook pre commit
pre-commit = [ pre-commit = [
"go mod tidy -go=1.23.5", "go mod tidy -go=1.24",
"go run mage.go GenerateAll",
"husky lint-staged", "husky lint-staged",
# "golangci-lint run", # "golangci-lint run",
] ]

View file

@ -4,6 +4,19 @@
"initialization_options": { "initialization_options": {
"local": "code.icb4dc0.de/prskr/supabase-operator" "local": "code.icb4dc0.de/prskr/supabase-operator"
} }
},
"golangci-lint": {
"initialization_options": {
"command": [
"go",
"tool",
"golangci-lint",
"run",
"--output.json.path",
"stderr",
"--issues-exit-code=1"
]
}
} }
} }
} }

47
.zed/tasks.json Normal file
View file

@ -0,0 +1,47 @@
[
{
"label": "Tilt Up",
"command": "tilt",
"args": ["up"],
"use_new_terminal": false,
"allow_concurrent_runs": false
},
{
"label": "Generate CRDs",
"command": "go",
"args": ["run", "mage.go", "CRDs"],
"use_new_terminal": false,
"allow_concurrent_runs": false,
"tags": ["mage", "generate"]
},
{
"label": "Generate CRD docs",
"command": "go",
"args": ["run", "mage.go", "CRDDocs"],
"use_new_terminal": false,
"allow_concurrent_runs": false,
"tags": ["mage", "generate"]
},
{
"label": "Update image meta",
"command": "go",
"args": ["run", "mage.go", "FetchImageMeta"],
"use_new_terminal": false,
"allow_concurrent_runs": false,
"tags": ["mage"]
},
{
"label": "Update DB migrations",
"command": "go",
"args": ["run", "mage.go", "FetchMigrations"],
"use_new_terminal": false,
"allow_concurrent_runs": false,
"tags": ["mage"]
},
{
"label": "Run all Go tests",
"command": "go tool gotestsum ./...",
"use_new_terminal": false,
"allow_concurrent_runs": false
}
]

View file

@ -1,5 +1,5 @@
# Build the manager binary # Build the manager binary
FROM golang:1.23.4 AS builder FROM golang:1.24-alpine AS builder
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
@ -16,10 +16,7 @@ COPY [ "go.*", "./" ]
COPY [ "api", "api" ] COPY [ "api", "api" ]
COPY [ "assets/migrations", "assets/migrations" ] COPY [ "assets/migrations", "assets/migrations" ]
COPY [ "cmd", "cmd" ] COPY [ "cmd", "cmd" ]
COPY [ "infrastructure", "infrastructure" ]
COPY [ "internal", "internal" ] COPY [ "internal", "internal" ]
COPY [ "magefiles", "magefiles" ]
COPY [ "tools", "tools" ]
# Build # Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command # the GOARCH has not a default value to allow the binary be built according to the host where the command

View file

@ -44,11 +44,11 @@ help: ## Display this help.
##@ Development ##@ Development
.PHONY: manifests .PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. manifests: ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
.PHONY: generate .PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. generate: ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
.PHONY: fmt .PHONY: fmt
@ -60,7 +60,7 @@ vet: ## Run go vet against code.
go vet ./... go vet ./...
.PHONY: test .PHONY: test
test: manifests generate fmt vet envtest ## Run tests. test: manifests generate fmt vet ## 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 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'. # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
@ -81,11 +81,11 @@ test-e2e: manifests generate fmt vet ## Run the e2e tests. Expected an isolated
go test ./test/e2e/ -v -ginkgo.v go test ./test/e2e/ -v -ginkgo.v
.PHONY: lint .PHONY: lint
lint: golangci-lint ## Run golangci-lint linter lint: ## Run golangci-lint linter
$(GOLANGCI_LINT) run $(GOLANGCI_LINT) run
.PHONY: lint-fix .PHONY: lint-fix
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes lint-fix: ## Run golangci-lint linter and perform fixes
$(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT) run --fix
##@ Build ##@ Build
@ -127,7 +127,7 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform
rm Dockerfile.cross rm Dockerfile.cross
.PHONY: build-installer .PHONY: build-installer
build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. build-installer: manifests generate ## Generate a consolidated YAML with CRDs and deployment.
mkdir -p dist mkdir -p dist
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default > dist/install.yaml $(KUSTOMIZE) build config/default > dist/install.yaml
@ -148,7 +148,7 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified
.PHONY: deploy .PHONY: deploy
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} cd config/manager && $(KUSTOMIZE) edit set image supabase-operator=${IMG}
$(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f -
.PHONY: undeploy .PHONY: undeploy
@ -164,49 +164,7 @@ $(LOCALBIN):
## Tool Binaries ## Tool Binaries
KUBECTL ?= kubectl KUBECTL ?= kubectl
KUSTOMIZE ?= $(LOCALBIN)/kustomize KUSTOMIZE ?= go tool kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen CONTROLLER_GEN ?= go tool controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest ENVTEST ?= go tool setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint GOLANGCI_LINT = go tool golangci-lint
## Tool Versions
KUSTOMIZE_VERSION ?= v5.5.0
CONTROLLER_TOOLS_VERSION ?= v0.16.4
ENVTEST_VERSION ?= release-0.19
GOLANGCI_LINT_VERSION ?= v1.61.0
.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
$(KUSTOMIZE): $(LOCALBIN)
$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))
.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
$(CONTROLLER_GEN): $(LOCALBIN)
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))
.PHONY: envtest
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
$(ENVTEST): $(LOCALBIN)
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))
.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
# $2 - package url which can be installed
# $3 - specific version of package
define go-install-tool
@[ -f "$(1)-$(3)" ] || { \
set -e; \
package=$(2)@$(3) ;\
echo "Downloading $${package}" ;\
rm -f $(1) || true ;\
GOBIN=$(LOCALBIN) go install $${package} ;\
mv $(1) $(1)-$(3) ;\
} ;\
ln -sf $(1)-$(3) $(1)
endef

View file

@ -37,7 +37,7 @@ This operator tries to be as un-opionionated as possible and thereofore does not
## Getting Started ## Getting Started
### Prerequisites ### Prerequisites
- go version v1.23.x+ - go version v1.24.x+
- docker version 27.+. - docker version 27.+.
- kubectl version v1.30.0+. - kubectl version v1.30.0+.
- Access to a Kubernetes v1.30.+ cluster. - Access to a Kubernetes v1.30.+ cluster.

View file

@ -4,7 +4,6 @@ load('ext://restart_process', 'docker_build_with_restart')
allow_k8s_contexts('kind-kind') allow_k8s_contexts('kind-kind')
k8s_yaml(kustomize('config/dev')) k8s_yaml(kustomize('config/dev'))
k8s_yaml(kustomize('config/samples'))
compile_cmd = 'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out/supabase-operator ./cmd/' compile_cmd = 'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out/supabase-operator ./cmd/'
@ -23,8 +22,10 @@ local_resource(
resource_deps=[] resource_deps=[]
) )
k8s_kind('Cluster', api_version='postgresql.cnpg.io/v1')
docker_build_with_restart( docker_build_with_restart(
'supabase-operator', 'controller',
'.', '.',
entrypoint=['/app/bin/supabase-operator'], entrypoint=['/app/bin/supabase-operator'],
dockerfile='dev/Dockerfile', dockerfile='dev/Dockerfile',
@ -40,10 +41,11 @@ k8s_resource('supabase-controller-manager')
k8s_resource( k8s_resource(
workload='supabase-control-plane', workload='supabase-control-plane',
port_forwards=18000, port_forwards=18000,
resource_deps=[]
) )
k8s_resource( k8s_resource(
objects=["cluster-example:Cluster:supabase-demo"], workload='cluster-example',
new_name='Postgres cluster', new_name='Postgres cluster',
port_forwards=5432 port_forwards=5432
) )
@ -62,7 +64,12 @@ k8s_resource(
k8s_resource( k8s_resource(
objects=["gateway-sample:APIGateway:supabase-demo"], objects=["gateway-sample:APIGateway:supabase-demo"],
extra_pod_selectors={"app.kubernetes.io/component": "api-gateway"}, extra_pod_selectors={"app.kubernetes.io/component": "api-gateway"},
port_forwards=[8000, 19000], port_forwards=[3000, 8000, 19000],
links=[
link("https://localhost:3000", "Studio"),
link("http://localhost:8000", "API"),
link("http://localhost:19000", "Envoy Admin Interface")
],
new_name='API Gateway', new_name='API Gateway',
resource_deps=[ resource_deps=[
'supabase-controller-manager' 'supabase-controller-manager'
@ -73,7 +80,6 @@ k8s_resource(
objects=["dashboard-sample:Dashboard:supabase-demo"], objects=["dashboard-sample:Dashboard:supabase-demo"],
extra_pod_selectors={"app.kubernetes.io/component": "dashboard", "app.kubernetes.io/name": "studio"}, extra_pod_selectors={"app.kubernetes.io/component": "dashboard", "app.kubernetes.io/name": "studio"},
discovery_strategy="selectors-only", discovery_strategy="selectors-only",
port_forwards=[3000],
new_name='Dashboard', new_name='Dashboard',
resource_deps=[ resource_deps=[
'supabase-controller-manager' 'supabase-controller-manager'

View file

@ -19,6 +19,7 @@ package v1alpha1
import ( import (
"iter" "iter"
"maps" "maps"
"strings"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -37,6 +38,40 @@ type ControlPlaneSpec struct {
Port uint16 `json:"port"` Port uint16 `json:"port"`
} }
type EnvoyLogLevel string
type EnvoyComponentLogLevel struct {
// Component - the component to set the log level for
// the component IDs can be found [here](https://github.com/envoyproxy/envoy/blob/main/source/common/common/logger.h#L36)
Component string `json:"component"`
// Level - the log level to set for the component
// +kubebuilder:validation:Enum=trace;debug;info;warning;error;critical;off
Level EnvoyLogLevel `json:"level"`
}
type EnvoyDebuggingOptions struct {
ComponentLogLevels []EnvoyComponentLogLevel `json:"componentLogLevels,omitempty"`
}
func (o *EnvoyDebuggingOptions) DebugLogging() string {
if o == nil || len(o.ComponentLogLevels) == 0 {
return ""
}
var builder strings.Builder
for i, lvl := range o.ComponentLogLevels {
if i > 0 {
builder.WriteString(",")
}
builder.WriteString(lvl.Component)
builder.WriteRune(':')
builder.WriteString(string(lvl.Level))
}
return builder.String()
}
type EnvoySpec struct { type EnvoySpec struct {
// NodeName - identifies the Envoy cluster within the current namespace // NodeName - identifies the Envoy cluster within the current namespace
// if not set, the name of the APIGateway resource will be used // if not set, the name of the APIGateway resource will be used
@ -45,15 +80,148 @@ type EnvoySpec struct {
// ControlPlane - configure the control plane where Envoy will retrieve its configuration from // ControlPlane - configure the control plane where Envoy will retrieve its configuration from
ControlPlane *ControlPlaneSpec `json:"controlPlane"` ControlPlane *ControlPlaneSpec `json:"controlPlane"`
// WorkloadTemplate - customize the Envoy deployment // WorkloadTemplate - customize the Envoy deployment
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"` WorkloadSpec *WorkloadSpec `json:"workloadSpec,omitempty"`
// DisableIPv6 - disable IPv6 for the Envoy instance
// this will force Envoy to use IPv4 for upstream hosts (mostly for the OAuth2 token endpoint)
DisableIPv6 bool `json:"disableIPv6,omitempty"`
Debugging *EnvoyDebuggingOptions `json:"debugging,omitempty"`
}
type TlsCertRef struct {
SecretName string `json:"secretName"`
// ServerCertKey - key in the secret that contains the server certificate
// +kubebuilder:default="tls.crt"
ServerCertKey string `json:"serverCertKey"`
// ServerKeyKey - key in the secret that contains the server private key
// +kubebuilder:default="tls.key"
ServerKeyKey string `json:"serverKeyKey"`
// CaCertKey - key in the secret that contains the CA certificate
// +kubebuilder:default="ca.crt"
CaCertKey string `json:"caCertKey,omitempty"`
}
type EndpointTlsSpec struct {
Cert *TlsCertRef `json:"cert"`
}
type ApiEndpointSpec struct {
// JWKSSelector - selector where the JWKS can be retrieved from to enable the API gateway to validate JWTs
JWKSSelector *corev1.SecretKeySelector `json:"jwks"`
// TLS - enable and configure TLS for the API endpoint
TLS *EndpointTlsSpec `json:"tls,omitempty"`
}
func (s *ApiEndpointSpec) TLSSpec() *EndpointTlsSpec {
if s == nil {
return nil
}
return s.TLS
}
type DashboardAuthType string
const (
DashboardAuthTypeNone DashboardAuthType = "none"
DashboardAuthTypeOAuth2 DashboardAuthType = "oauth2"
DashboardAuthTypeBasic DashboardAuthType = "basic"
)
type DashboardOAuth2Spec struct {
// OpenIDIssuer - if set the defaulter will fetch the discovery document and fill
// TokenEndpoint and AuthorizationEndpoint based on the discovery document
OpenIDIssuer string `json:"openIdIssuer,omitempty"`
// TokenEndpoint - endpoint where Envoy will retrieve the OAuth2 access and identity token from
TokenEndpoint string `json:"tokenEndpoint,omitempty"`
// AuthorizationEndpoint - endpoint where the user will be redirected to authenticate
AuthorizationEndpoint string `json:"authorizationEndpoint,omitempty"`
// ClientID - client ID to authenticate with the OAuth2 provider
ClientID string `json:"clientId"`
// Scopes - scopes to request from the OAuth2 provider (e.g. "openid", "profile", ...) - optional
Scopes []string `json:"scopes,omitempty"`
// Resources - resources to request from the OAuth2 provider (e.g. "user", "email", ...) - optional
Resources []string `json:"resources,omitempty"`
// ClientSecretRef - reference to the secret that contains the client secret
ClientSecretRef *corev1.SecretKeySelector `json:"clientSecretRef"`
}
type DashboardBasicAuthSpec struct {
// UsersInline - [htpasswd format](https://httpd.apache.org/docs/2.4/programs/htpasswd.html)
// +kubebuilder:validation:items:Pattern="^[\\w_.]+:\\{SHA\\}[A-z0-9]+=*$"
UsersInline []string `json:"usersInline,omitempty"`
// PlaintextUsersSecretRef - name of a secret that contains plaintext credentials in key-value form
// if not empty, credentials will be merged with inline users
PlaintextUsersSecretRef string `json:"plaintextUsersSecretRef,omitempty"`
}
type DashboardAuthSpec struct {
// OAuth2 - configure oauth2 authentication for the dashhboard listener
// if configured, will be preferred over Basic authentication configuration
// effectively disabling basic auth
OAuth2 *DashboardOAuth2Spec `json:"oauth2,omitempty"`
// Basic - HTTP basic auth configuration, this should only be used in exceptions
// e.g. during evaluations or for local development
// only used if no other authentication is configured
Basic *DashboardBasicAuthSpec `json:"basic,omitempty"`
}
type DashboardEndpointSpec struct {
// Auth - configure authentication for the dashboard endpoint
Auth *DashboardAuthSpec `json:"auth,omitempty"`
// TLS - enable and configure TLS for the Dashboard endpoint
TLS *EndpointTlsSpec `json:"tls,omitempty"`
}
func (s *DashboardEndpointSpec) TLSSpec() *EndpointTlsSpec {
if s == nil {
return nil
}
return s.TLS
}
func (s *DashboardEndpointSpec) AuthType() DashboardAuthType {
if s == nil || s.Auth == nil {
return DashboardAuthTypeNone
}
if s.Auth.OAuth2 != nil {
return DashboardAuthTypeOAuth2
}
if s.Auth.Basic != nil {
return DashboardAuthTypeBasic
}
return DashboardAuthTypeNone
}
func (s *DashboardEndpointSpec) OAuth2() *DashboardOAuth2Spec {
if s == nil || s.Auth == nil {
return nil
}
return s.Auth.OAuth2
}
func (s *DashboardEndpointSpec) Basic() *DashboardBasicAuthSpec {
if s == nil || s.Auth == nil {
return nil
}
return s.Auth.Basic
} }
// APIGatewaySpec defines the desired state of APIGateway. // APIGatewaySpec defines the desired state of APIGateway.
type APIGatewaySpec struct { type APIGatewaySpec struct {
// Envoy - configure the envoy instance and most importantly the control-plane // Envoy - configure the envoy instance and most importantly the control-plane
Envoy *EnvoySpec `json:"envoy"` Envoy *EnvoySpec `json:"envoy"`
// JWKSSelector - selector where the JWKS can be retrieved from to enable the API gateway to validate JWTs // ApiEndpoint - Configure the endpoint for all API routes
JWKSSelector *corev1.SecretKeySelector `json:"jwks"` // this includes the JWT configuration
ApiEndpoint *ApiEndpointSpec `json:"apiEndpoint,omitempty"`
// DashboardEndpoint - Configure the endpoint for the Supabase dashboard (studio)
// this includes optional authentication (basic or Oauth2) for the dashboard
DashboardEndpoint *DashboardEndpointSpec `json:"dashboardEndpoint,omitempty"`
// ServiceSelector - selector to match all Supabase services (or in fact EndpointSlices) that should be considered for this APIGateway // ServiceSelector - selector to match all Supabase services (or in fact EndpointSlices) that should be considered for this APIGateway
// +kubebuilder:default={"matchExpressions":{{"key": "app.kubernetes.io/part-of", "operator":"In", "values":{"supabase"}},{"key":"supabase.k8s.icb4dc0.de/api-gateway-target","operator":"Exists"}}} // +kubebuilder:default={"matchExpressions":{{"key": "app.kubernetes.io/part-of", "operator":"In", "values":{"supabase"}},{"key":"supabase.k8s.icb4dc0.de/api-gateway-target","operator":"Exists"}}}
ServiceSelector *metav1.LabelSelector `json:"serviceSelector"` ServiceSelector *metav1.LabelSelector `json:"serviceSelector"`
@ -63,8 +231,7 @@ type APIGatewaySpec struct {
} }
type EnvoyStatus struct { type EnvoyStatus struct {
ConfigVersion string `json:"configVersion,omitempty"` ResourceHash []byte `json:"resourceHash,omitempty"`
ResourceHash []byte `json:"resourceHash,omitempty"`
} }
// APIGatewayStatus defines the observed state of APIGateway. // APIGatewayStatus defines the observed state of APIGateway.
@ -77,7 +244,7 @@ type APIGatewayStatus struct {
// +kubebuilder:subresource:status // +kubebuilder:subresource:status
// APIGateway is the Schema for the apigateways API. // APIGateway is the Schema for the apigateways API.
// +kubebuilder:printcolumn:name="EnvoyConfigVersion",type=string,JSONPath=`.status.envoy.configVersion` // +kubebuilder:printcolumn:name="EnvoyConfigVersion",type=string,JSONPath=`.status.envoy.resourceHash`
type APIGateway struct { type APIGateway struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"` metav1.ObjectMeta `json:"metadata,omitempty"`
@ -88,7 +255,7 @@ type APIGateway struct {
func (g APIGateway) JwksSecretMeta() metav1.ObjectMeta { func (g APIGateway) JwksSecretMeta() metav1.ObjectMeta {
return metav1.ObjectMeta{ return metav1.ObjectMeta{
Name: g.Spec.JWKSSelector.Name, Name: g.Spec.ApiEndpoint.JWKSSelector.Name,
Namespace: g.Namespace, Namespace: g.Namespace,
Labels: maps.Clone(g.Labels), Labels: maps.Clone(g.Labels),
} }

View file

@ -91,16 +91,16 @@ type ContainerTemplate struct {
AdditionalEnv []corev1.EnvVar `json:"additionalEnv,omitempty"` AdditionalEnv []corev1.EnvVar `json:"additionalEnv,omitempty"`
} }
type WorkloadTemplate struct { type WorkloadSpec struct {
Replicas *int32 `json:"replicas,omitempty"` Replicas *int32 `json:"replicas,omitempty"`
SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"`
AdditionalLabels map[string]string `json:"additionalLabels,omitempty"` AdditionalLabels map[string]string `json:"additionalLabels,omitempty"`
// Workload - customize the container template of the workload // ContainerSpec - customize the container template of the workload
Workload *ContainerTemplate `json:"workload,omitempty"` ContainerSpec *ContainerTemplate `json:"container,omitempty"`
AdditionalVolumes []corev1.Volume `json:"additionalVolumes,omitempty"` AdditionalVolumes []corev1.Volume `json:"additionalVolumes,omitempty"`
} }
func (t *WorkloadTemplate) ReplicaCount() *int32 { func (t *WorkloadSpec) ReplicaCount() *int32 {
if t != nil && t.Replicas != nil { if t != nil && t.Replicas != nil {
return t.Replicas return t.Replicas
} }
@ -108,20 +108,20 @@ func (t *WorkloadTemplate) ReplicaCount() *int32 {
return nil return nil
} }
func (t *WorkloadTemplate) MergeEnv(basicEnv []corev1.EnvVar) []corev1.EnvVar { func (t *WorkloadSpec) MergeEnv(basicEnv []corev1.EnvVar) []corev1.EnvVar {
if t == nil || t.Workload == nil || len(t.Workload.AdditionalEnv) == 0 { if t == nil || t.ContainerSpec == nil || len(t.ContainerSpec.AdditionalEnv) == 0 {
return basicEnv return basicEnv
} }
existingKeys := make(map[string]bool, len(basicEnv)+len(t.Workload.AdditionalEnv)) existingKeys := make(map[string]bool, len(basicEnv)+len(t.ContainerSpec.AdditionalEnv))
merged := append(make([]corev1.EnvVar, 0, len(basicEnv)+len(t.Workload.AdditionalEnv)), basicEnv...) merged := append(make([]corev1.EnvVar, 0, len(basicEnv)+len(t.ContainerSpec.AdditionalEnv)), basicEnv...)
for _, v := range basicEnv { for _, v := range basicEnv {
existingKeys[v.Name] = true existingKeys[v.Name] = true
} }
for _, v := range t.Workload.AdditionalEnv { for _, v := range t.ContainerSpec.AdditionalEnv {
if _, alreadyPresent := existingKeys[v.Name]; alreadyPresent { if _, alreadyPresent := existingKeys[v.Name]; alreadyPresent {
continue continue
} }
@ -132,7 +132,7 @@ func (t *WorkloadTemplate) MergeEnv(basicEnv []corev1.EnvVar) []corev1.EnvVar {
return merged return merged
} }
func (t *WorkloadTemplate) MergeLabels(initial map[string]string, toAppend ...map[string]string) map[string]string { func (t *WorkloadSpec) MergeLabels(initial map[string]string, toAppend ...map[string]string) map[string]string {
result := make(map[string]string) result := make(map[string]string)
maps.Copy(result, initial) maps.Copy(result, initial)
@ -156,47 +156,47 @@ func (t *WorkloadTemplate) MergeLabels(initial map[string]string, toAppend ...ma
return result return result
} }
func (t *WorkloadTemplate) Image(defaultImage string) string { func (t *WorkloadSpec) Image(defaultImage string) string {
if t != nil && t.Workload != nil && t.Workload.Image != "" { if t != nil && t.ContainerSpec != nil && t.ContainerSpec.Image != "" {
return t.Workload.Image return t.ContainerSpec.Image
} }
return defaultImage return defaultImage
} }
func (t *WorkloadTemplate) ImagePullPolicy() corev1.PullPolicy { func (t *WorkloadSpec) ImagePullPolicy() corev1.PullPolicy {
if t != nil && t.Workload != nil && t.Workload.PullPolicy != "" { if t != nil && t.ContainerSpec != nil && t.ContainerSpec.PullPolicy != "" {
return t.Workload.PullPolicy return t.ContainerSpec.PullPolicy
} }
return corev1.PullIfNotPresent return corev1.PullIfNotPresent
} }
func (t *WorkloadTemplate) PullSecrets() []corev1.LocalObjectReference { func (t *WorkloadSpec) PullSecrets() []corev1.LocalObjectReference {
if t != nil && t.Workload != nil && len(t.Workload.ImagePullSecrets) > 0 { if t != nil && t.ContainerSpec != nil && len(t.ContainerSpec.ImagePullSecrets) > 0 {
return t.Workload.ImagePullSecrets return t.ContainerSpec.ImagePullSecrets
} }
return nil return nil
} }
func (t *WorkloadTemplate) Resources() corev1.ResourceRequirements { func (t *WorkloadSpec) Resources() corev1.ResourceRequirements {
if t != nil && t.Workload != nil { if t != nil && t.ContainerSpec != nil {
return t.Workload.Resources return t.ContainerSpec.Resources
} }
return corev1.ResourceRequirements{} return corev1.ResourceRequirements{}
} }
func (t *WorkloadTemplate) AdditionalVolumeMounts(defaultMounts ...corev1.VolumeMount) []corev1.VolumeMount { func (t *WorkloadSpec) AdditionalVolumeMounts(defaultMounts ...corev1.VolumeMount) []corev1.VolumeMount {
if t != nil && t.Workload != nil { if t != nil && t.ContainerSpec != nil {
return append(defaultMounts, t.Workload.VolumeMounts...) return append(defaultMounts, t.ContainerSpec.VolumeMounts...)
} }
return defaultMounts return defaultMounts
} }
func (t *WorkloadTemplate) Volumes(defaultVolumes ...corev1.Volume) []corev1.Volume { func (t *WorkloadSpec) Volumes(defaultVolumes ...corev1.Volume) []corev1.Volume {
if t == nil { if t == nil {
return defaultVolumes return defaultVolumes
} }
@ -204,7 +204,7 @@ func (t *WorkloadTemplate) Volumes(defaultVolumes ...corev1.Volume) []corev1.Vol
return append(defaultVolumes, t.AdditionalVolumes...) return append(defaultVolumes, t.AdditionalVolumes...)
} }
func (t *WorkloadTemplate) PodSecurityContext() *corev1.PodSecurityContext { func (t *WorkloadSpec) PodSecurityContext() *corev1.PodSecurityContext {
if t != nil && t.SecurityContext != nil { if t != nil && t.SecurityContext != nil {
return t.SecurityContext return t.SecurityContext
} }
@ -214,9 +214,9 @@ func (t *WorkloadTemplate) PodSecurityContext() *corev1.PodSecurityContext {
} }
} }
func (t *WorkloadTemplate) ContainerSecurityContext(uid, gid int64) *corev1.SecurityContext { func (t *WorkloadSpec) ContainerSecurityContext(uid, gid int64) *corev1.SecurityContext {
if t != nil && t.Workload != nil && t.Workload.SecurityContext != nil { if t != nil && t.ContainerSpec != nil && t.ContainerSpec.SecurityContext != nil {
return t.Workload.SecurityContext return t.ContainerSpec.SecurityContext
} }
return &corev1.SecurityContext{ return &corev1.SecurityContext{

View file

@ -17,6 +17,7 @@ limitations under the License.
package v1alpha1 package v1alpha1
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
@ -25,6 +26,7 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"time"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -59,7 +61,7 @@ type DatabaseRoles struct {
type Database struct { type Database struct {
DSN *string `json:"dsn,omitempty"` DSN *string `json:"dsn,omitempty"`
DSNSecretRef *corev1.SecretKeySelector `json:"dsnSecretRef,omitempty"` DSNSecretRef *corev1.SecretKeySelector `json:"dsnSecretRef"`
Roles DatabaseRoles `json:"roles,omitempty"` Roles DatabaseRoles `json:"roles,omitempty"`
} }
@ -167,8 +169,8 @@ type PostgrestSpec struct {
// MaxRows - maximum number of rows PostgREST will load at a time // MaxRows - maximum number of rows PostgREST will load at a time
// +kubebuilder:default=1000 // +kubebuilder:default=1000
MaxRows int `json:"maxRows,omitempty"` MaxRows int `json:"maxRows,omitempty"`
// WorkloadTemplate - customize the PostgREST workload // WorkloadSpec - customize the PostgREST workload
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"` WorkloadSpec *WorkloadSpec `json:"workloadSpec,omitempty"`
} }
type AuthProviderMeta struct { type AuthProviderMeta struct {
@ -365,12 +367,12 @@ func (p *AuthProviders) Vars(apiExternalURL string) []corev1.EnvVar {
} }
type AuthSpec struct { type AuthSpec struct {
AdditionalRedirectUrls []string `json:"additionalRedirectUrls,omitempty"` AdditionalRedirectUrls []string `json:"additionalRedirectUrls,omitempty"`
DisableSignup *bool `json:"disableSignup,omitempty"` DisableSignup *bool `json:"disableSignup,omitempty"`
AnonymousUsersEnabled *bool `json:"anonymousUsersEnabled,omitempty"` AnonymousUsersEnabled *bool `json:"anonymousUsersEnabled,omitempty"`
Providers *AuthProviders `json:"providers,omitempty"` Providers *AuthProviders `json:"providers,omitempty"`
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"` WorkloadTemplate *WorkloadSpec `json:"workloadTemplate,omitempty"`
EmailSignupDisabled *bool `json:"emailSignupDisabled,omitempty"` EmailSignupDisabled *bool `json:"emailSignupDisabled,omitempty"`
} }
// CoreSpec defines the desired state of Core. // CoreSpec defines the desired state of Core.
@ -387,20 +389,89 @@ type CoreSpec struct {
Auth *AuthSpec `json:"auth,omitempty"` Auth *AuthSpec `json:"auth,omitempty"`
} }
type MigrationStatus map[string]metav1.Time type MigrationConditionStatus string
func (s MigrationStatus) IsApplied(name string) bool { const (
_, ok := s[name] MigrationConditionStatusApplied MigrationConditionStatus = "Applied"
return ok MigrationConditionStatusFailed MigrationConditionStatus = "Failed"
} )
func (s MigrationStatus) Record(name string) { type MigrationScriptCondition struct {
s[name] = metav1.Now() // Name - file name of the migration script
Name string `json:"name"`
// Hash - SHA256 hash of the script when it was last successfully applied
Hash []byte `json:"hash"`
// Status - whether the migration was applied or not
// +kubebuilder:validation:Enum=Applied;Failed
Status MigrationConditionStatus `json:"status"`
// LastProbeTime - last time the operator tried to execute the migration script
LastProbeTime metav1.Time `json:"lastProbeTime,omitempty"`
// LastTransitionTime - last time the condition transitioned from one status to another
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
// Reason - one-word, CamcelCase reason for the condition's last transition
Reason string `json:"reason,omitempty"`
// Message - human-readable message indicating details about the last transition
Message string `json:"message,omitempty"`
} }
type DatabaseStatus struct { type DatabaseStatus struct {
AppliedMigrations MigrationStatus `json:"appliedMigrations,omitempty"` MigrationConditions []MigrationScriptCondition `json:"migrationConditions,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
Roles map[string][]byte `json:"roles,omitempty"` Roles map[string][]byte `json:"roles,omitempty"`
}
func (s DatabaseStatus) IsMigrationUpToDate(name string, hash []byte) (found bool, upToDate bool) {
for _, cond := range s.MigrationConditions {
if cond.Name == name {
return true, bytes.Equal(cond.Hash, hash)
}
}
return false, false
}
func (s *DatabaseStatus) RecordMigrationCondition(name string, hash []byte, err error) error {
var (
now = time.Now()
newStatus = MigrationConditionStatusApplied
lastProbeTime = metav1.NewTime(now)
lastTransitionTime = metav1.NewTime(now)
message string
)
if err != nil {
newStatus = MigrationConditionStatusFailed
message = err.Error()
}
for idx, cond := range s.MigrationConditions {
if cond.Name == name {
lastTransitionTime = cond.LastTransitionTime
if cond.Status != newStatus {
lastTransitionTime = metav1.NewTime(now)
}
cond.Hash = hash
cond.Status = newStatus
cond.LastProbeTime = lastProbeTime
cond.LastTransitionTime = lastTransitionTime
cond.Reason = "Outdated"
cond.Message = message
s.MigrationConditions[idx] = cond
return err
}
}
s.MigrationConditions = append(s.MigrationConditions, MigrationScriptCondition{
Name: name,
Hash: hash,
Status: newStatus,
LastProbeTime: lastProbeTime,
LastTransitionTime: lastTransitionTime,
Message: message,
})
return err
} }
type CoreConditionType string type CoreConditionType string

View file

@ -24,7 +24,7 @@ import (
type StudioSpec struct { type StudioSpec struct {
JWT *JwtSpec `json:"jwt,omitempty"` JWT *JwtSpec `json:"jwt,omitempty"`
// WorkloadTemplate - customize the studio deployment // WorkloadTemplate - customize the studio deployment
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"` WorkloadSpec *WorkloadSpec `json:"workloadSpec,omitempty"`
// GatewayServiceSelector - selector to find the service for the API gateway // GatewayServiceSelector - selector to find the service for the API gateway
// Required to configure the API URL in the studio deployment // Required to configure the API URL in the studio deployment
// If you don't run multiple APIGateway instances in the same namespaces, the default will be fine // If you don't run multiple APIGateway instances in the same namespaces, the default will be fine
@ -37,7 +37,7 @@ type StudioSpec struct {
type PGMetaSpec struct { type PGMetaSpec struct {
// WorkloadTemplate - customize the pg-meta deployment // WorkloadTemplate - customize the pg-meta deployment
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"` WorkloadSpec *WorkloadSpec `json:"workloadSpec,omitempty"`
} }
type DbCredentialsReference struct { type DbCredentialsReference struct {

View file

@ -191,7 +191,7 @@ type StorageApiSpec struct {
// UploadTemp - configure the emptyDir for storing intermediate files during uploads // UploadTemp - configure the emptyDir for storing intermediate files during uploads
UploadTemp *UploadTempSpec `json:"uploadTemp,omitempty"` UploadTemp *UploadTempSpec `json:"uploadTemp,omitempty"`
// WorkloadTemplate - customize the Storage API workload // WorkloadTemplate - customize the Storage API workload
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"` WorkloadSpec *WorkloadSpec `json:"workloadSpec,omitempty"`
} }
type ImageProxySpec struct { type ImageProxySpec struct {
@ -199,7 +199,7 @@ type ImageProxySpec struct {
Enable bool `json:"enable,omitempty"` Enable bool `json:"enable,omitempty"`
EnabledWebPDetection bool `json:"enableWebPDetection,omitempty"` EnabledWebPDetection bool `json:"enableWebPDetection,omitempty"`
// WorkloadTemplate - customize the image proxy workload // WorkloadTemplate - customize the image proxy workload
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"` WorkloadSpec *WorkloadSpec `json:"workloadSpec,omitempty"`
} }
// StorageSpec defines the desired state of Storage. // StorageSpec defines the desired state of Storage.

View file

@ -93,9 +93,14 @@ func (in *APIGatewaySpec) DeepCopyInto(out *APIGatewaySpec) {
*out = new(EnvoySpec) *out = new(EnvoySpec)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.JWKSSelector != nil { if in.ApiEndpoint != nil {
in, out := &in.JWKSSelector, &out.JWKSSelector in, out := &in.ApiEndpoint, &out.ApiEndpoint
*out = new(v1.SecretKeySelector) *out = new(ApiEndpointSpec)
(*in).DeepCopyInto(*out)
}
if in.DashboardEndpoint != nil {
in, out := &in.DashboardEndpoint, &out.DashboardEndpoint
*out = new(DashboardEndpointSpec)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.ServiceSelector != nil { if in.ServiceSelector != nil {
@ -147,6 +152,31 @@ func (in *APIGatewayStatus) DeepCopy() *APIGatewayStatus {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApiEndpointSpec) DeepCopyInto(out *ApiEndpointSpec) {
*out = *in
if in.JWKSSelector != nil {
in, out := &in.JWKSSelector, &out.JWKSSelector
*out = new(v1.SecretKeySelector)
(*in).DeepCopyInto(*out)
}
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(EndpointTlsSpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApiEndpointSpec.
func (in *ApiEndpointSpec) DeepCopy() *ApiEndpointSpec {
if in == nil {
return nil
}
out := new(ApiEndpointSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuthProviderMeta) DeepCopyInto(out *AuthProviderMeta) { func (in *AuthProviderMeta) DeepCopyInto(out *AuthProviderMeta) {
*out = *in *out = *in
@ -222,7 +252,7 @@ func (in *AuthSpec) DeepCopyInto(out *AuthSpec) {
} }
if in.WorkloadTemplate != nil { if in.WorkloadTemplate != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
*out = new(WorkloadTemplate) *out = new(WorkloadSpec)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.EmailSignupDisabled != nil { if in.EmailSignupDisabled != nil {
@ -465,6 +495,51 @@ func (in *Dashboard) DeepCopyObject() runtime.Object {
return nil return nil
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardAuthSpec) DeepCopyInto(out *DashboardAuthSpec) {
*out = *in
if in.OAuth2 != nil {
in, out := &in.OAuth2, &out.OAuth2
*out = new(DashboardOAuth2Spec)
(*in).DeepCopyInto(*out)
}
if in.Basic != nil {
in, out := &in.Basic, &out.Basic
*out = new(DashboardBasicAuthSpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardAuthSpec.
func (in *DashboardAuthSpec) DeepCopy() *DashboardAuthSpec {
if in == nil {
return nil
}
out := new(DashboardAuthSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardBasicAuthSpec) DeepCopyInto(out *DashboardBasicAuthSpec) {
*out = *in
if in.UsersInline != nil {
in, out := &in.UsersInline, &out.UsersInline
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardBasicAuthSpec.
func (in *DashboardBasicAuthSpec) DeepCopy() *DashboardBasicAuthSpec {
if in == nil {
return nil
}
out := new(DashboardBasicAuthSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardDbSpec) DeepCopyInto(out *DashboardDbSpec) { func (in *DashboardDbSpec) DeepCopyInto(out *DashboardDbSpec) {
*out = *in *out = *in
@ -485,6 +560,31 @@ func (in *DashboardDbSpec) DeepCopy() *DashboardDbSpec {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardEndpointSpec) DeepCopyInto(out *DashboardEndpointSpec) {
*out = *in
if in.Auth != nil {
in, out := &in.Auth, &out.Auth
*out = new(DashboardAuthSpec)
(*in).DeepCopyInto(*out)
}
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(EndpointTlsSpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardEndpointSpec.
func (in *DashboardEndpointSpec) DeepCopy() *DashboardEndpointSpec {
if in == nil {
return nil
}
out := new(DashboardEndpointSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardList) DeepCopyInto(out *DashboardList) { func (in *DashboardList) DeepCopyInto(out *DashboardList) {
*out = *in *out = *in
@ -517,6 +617,36 @@ func (in *DashboardList) DeepCopyObject() runtime.Object {
return nil return nil
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardOAuth2Spec) DeepCopyInto(out *DashboardOAuth2Spec) {
*out = *in
if in.Scopes != nil {
in, out := &in.Scopes, &out.Scopes
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Resources != nil {
in, out := &in.Resources, &out.Resources
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ClientSecretRef != nil {
in, out := &in.ClientSecretRef, &out.ClientSecretRef
*out = new(v1.SecretKeySelector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardOAuth2Spec.
func (in *DashboardOAuth2Spec) DeepCopy() *DashboardOAuth2Spec {
if in == nil {
return nil
}
out := new(DashboardOAuth2Spec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardSpec) DeepCopyInto(out *DashboardSpec) { func (in *DashboardSpec) DeepCopyInto(out *DashboardSpec) {
*out = *in *out = *in
@ -622,11 +752,11 @@ func (in *DatabaseRolesSecrets) DeepCopy() *DatabaseRolesSecrets {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DatabaseStatus) DeepCopyInto(out *DatabaseStatus) { func (in *DatabaseStatus) DeepCopyInto(out *DatabaseStatus) {
*out = *in *out = *in
if in.AppliedMigrations != nil { if in.MigrationConditions != nil {
in, out := &in.AppliedMigrations, &out.AppliedMigrations in, out := &in.MigrationConditions, &out.MigrationConditions
*out = make(MigrationStatus, len(*in)) *out = make([]MigrationScriptCondition, len(*in))
for key, val := range *in { for i := range *in {
(*out)[key] = *val.DeepCopy() (*in)[i].DeepCopyInto(&(*out)[i])
} }
} }
if in.Roles != nil { if in.Roles != nil {
@ -728,6 +858,61 @@ func (in *EmailAuthSmtpSpec) DeepCopy() *EmailAuthSmtpSpec {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EndpointTlsSpec) DeepCopyInto(out *EndpointTlsSpec) {
*out = *in
if in.Cert != nil {
in, out := &in.Cert, &out.Cert
*out = new(TlsCertRef)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointTlsSpec.
func (in *EndpointTlsSpec) DeepCopy() *EndpointTlsSpec {
if in == nil {
return nil
}
out := new(EndpointTlsSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EnvoyComponentLogLevel) DeepCopyInto(out *EnvoyComponentLogLevel) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvoyComponentLogLevel.
func (in *EnvoyComponentLogLevel) DeepCopy() *EnvoyComponentLogLevel {
if in == nil {
return nil
}
out := new(EnvoyComponentLogLevel)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EnvoyDebuggingOptions) DeepCopyInto(out *EnvoyDebuggingOptions) {
*out = *in
if in.ComponentLogLevels != nil {
in, out := &in.ComponentLogLevels, &out.ComponentLogLevels
*out = make([]EnvoyComponentLogLevel, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvoyDebuggingOptions.
func (in *EnvoyDebuggingOptions) DeepCopy() *EnvoyDebuggingOptions {
if in == nil {
return nil
}
out := new(EnvoyDebuggingOptions)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EnvoySpec) DeepCopyInto(out *EnvoySpec) { func (in *EnvoySpec) DeepCopyInto(out *EnvoySpec) {
*out = *in *out = *in
@ -736,9 +921,14 @@ func (in *EnvoySpec) DeepCopyInto(out *EnvoySpec) {
*out = new(ControlPlaneSpec) *out = new(ControlPlaneSpec)
**out = **in **out = **in
} }
if in.WorkloadTemplate != nil { if in.WorkloadSpec != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate in, out := &in.WorkloadSpec, &out.WorkloadSpec
*out = new(WorkloadTemplate) *out = new(WorkloadSpec)
(*in).DeepCopyInto(*out)
}
if in.Debugging != nil {
in, out := &in.Debugging, &out.Debugging
*out = new(EnvoyDebuggingOptions)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
} }
@ -808,9 +998,9 @@ func (in *GithubAuthProvider) DeepCopy() *GithubAuthProvider {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageProxySpec) DeepCopyInto(out *ImageProxySpec) { func (in *ImageProxySpec) DeepCopyInto(out *ImageProxySpec) {
*out = *in *out = *in
if in.WorkloadTemplate != nil { if in.WorkloadSpec != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate in, out := &in.WorkloadSpec, &out.WorkloadSpec
*out = new(WorkloadTemplate) *out = new(WorkloadSpec)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
} }
@ -856,24 +1046,25 @@ func (in *JwtSpec) DeepCopy() *JwtSpec {
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in MigrationStatus) DeepCopyInto(out *MigrationStatus) { func (in *MigrationScriptCondition) DeepCopyInto(out *MigrationScriptCondition) {
{ *out = *in
in := &in if in.Hash != nil {
*out = make(MigrationStatus, len(*in)) in, out := &in.Hash, &out.Hash
for key, val := range *in { *out = make([]byte, len(*in))
(*out)[key] = *val.DeepCopy() copy(*out, *in)
}
} }
in.LastProbeTime.DeepCopyInto(&out.LastProbeTime)
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MigrationStatus. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MigrationScriptCondition.
func (in MigrationStatus) DeepCopy() MigrationStatus { func (in *MigrationScriptCondition) DeepCopy() *MigrationScriptCondition {
if in == nil { if in == nil {
return nil return nil
} }
out := new(MigrationStatus) out := new(MigrationScriptCondition)
in.DeepCopyInto(out) in.DeepCopyInto(out)
return *out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
@ -899,9 +1090,9 @@ func (in *OAuthProvider) DeepCopy() *OAuthProvider {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PGMetaSpec) DeepCopyInto(out *PGMetaSpec) { func (in *PGMetaSpec) DeepCopyInto(out *PGMetaSpec) {
*out = *in *out = *in
if in.WorkloadTemplate != nil { if in.WorkloadSpec != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate in, out := &in.WorkloadSpec, &out.WorkloadSpec
*out = new(WorkloadTemplate) *out = new(WorkloadSpec)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
} }
@ -945,9 +1136,9 @@ func (in *PostgrestSpec) DeepCopyInto(out *PostgrestSpec) {
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.WorkloadTemplate != nil { if in.WorkloadSpec != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate in, out := &in.WorkloadSpec, &out.WorkloadSpec
*out = new(WorkloadTemplate) *out = new(WorkloadSpec)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
} }
@ -1104,9 +1295,9 @@ func (in *StorageApiSpec) DeepCopyInto(out *StorageApiSpec) {
*out = new(UploadTempSpec) *out = new(UploadTempSpec)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.WorkloadTemplate != nil { if in.WorkloadSpec != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate in, out := &in.WorkloadSpec, &out.WorkloadSpec
*out = new(WorkloadTemplate) *out = new(WorkloadSpec)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
} }
@ -1197,9 +1388,9 @@ func (in *StudioSpec) DeepCopyInto(out *StudioSpec) {
*out = new(JwtSpec) *out = new(JwtSpec)
**out = **in **out = **in
} }
if in.WorkloadTemplate != nil { if in.WorkloadSpec != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate in, out := &in.WorkloadSpec, &out.WorkloadSpec
*out = new(WorkloadTemplate) *out = new(WorkloadSpec)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.GatewayServiceMatchLabels != nil { if in.GatewayServiceMatchLabels != nil {
@ -1221,6 +1412,21 @@ func (in *StudioSpec) DeepCopy() *StudioSpec {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TlsCertRef) DeepCopyInto(out *TlsCertRef) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TlsCertRef.
func (in *TlsCertRef) DeepCopy() *TlsCertRef {
if in == nil {
return nil
}
out := new(TlsCertRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UploadTempSpec) DeepCopyInto(out *UploadTempSpec) { func (in *UploadTempSpec) DeepCopyInto(out *UploadTempSpec) {
*out = *in *out = *in
@ -1242,7 +1448,7 @@ func (in *UploadTempSpec) DeepCopy() *UploadTempSpec {
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WorkloadTemplate) DeepCopyInto(out *WorkloadTemplate) { func (in *WorkloadSpec) DeepCopyInto(out *WorkloadSpec) {
*out = *in *out = *in
if in.Replicas != nil { if in.Replicas != nil {
in, out := &in.Replicas, &out.Replicas in, out := &in.Replicas, &out.Replicas
@ -1261,8 +1467,8 @@ func (in *WorkloadTemplate) DeepCopyInto(out *WorkloadTemplate) {
(*out)[key] = val (*out)[key] = val
} }
} }
if in.Workload != nil { if in.ContainerSpec != nil {
in, out := &in.Workload, &out.Workload in, out := &in.ContainerSpec, &out.ContainerSpec
*out = new(ContainerTemplate) *out = new(ContainerTemplate)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
@ -1275,12 +1481,12 @@ func (in *WorkloadTemplate) DeepCopyInto(out *WorkloadTemplate) {
} }
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadTemplate. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadSpec.
func (in *WorkloadTemplate) DeepCopy() *WorkloadTemplate { func (in *WorkloadSpec) DeepCopy() *WorkloadSpec {
if in == nil { if in == nil {
return nil return nil
} }
out := new(WorkloadTemplate) out := new(WorkloadSpec)
in.DeepCopyInto(out) in.DeepCopyInto(out)
return out return out
} }

View file

@ -18,7 +18,15 @@ grant pg_read_all_data to supabase_read_only_user;
create schema if not exists extensions; create schema if not exists extensions;
create extension if not exists "uuid-ossp" with schema extensions; create extension if not exists "uuid-ossp" with schema extensions;
create extension if not exists pgcrypto with schema extensions; create extension if not exists pgcrypto with schema extensions;
create extension if not exists pgjwt with schema extensions; do $$
begin
if exists (select 1 from pg_available_extensions where name = 'pgjwt') then
if not exists (select 1 from pg_extension where extname = 'pgjwt') then
create extension if not exists pgjwt with schema "extensions" cascade;
end if;
end if;
end $$;
-- Set up auth roles for the developer -- Set up auth roles for the developer
create role anon nologin noinherit; create role anon nologin noinherit;

View file

@ -17,6 +17,7 @@ limitations under the License.
package migrations package migrations
import ( import (
"crypto/sha256"
"embed" "embed"
"fmt" "fmt"
"io/fs" "io/fs"
@ -32,6 +33,7 @@ var migrationsFS embed.FS
type Script struct { type Script struct {
FileName string FileName string
Content string Content string
Hash []byte
} }
func InitScripts() iter.Seq2[Script, error] { func InitScripts() iter.Seq2[Script, error] {
@ -49,10 +51,18 @@ func RoleCreationScript(roleName string) (Script, error) {
return Script{}, err return Script{}, err
} }
return Script{fileName, string(content)}, nil hash := sha256.New()
_, _ = hash.Write(content)
return Script{
FileName: fileName,
Content: string(content),
Hash: hash.Sum(nil),
}, nil
} }
func readScripts(dir string) iter.Seq2[Script, error] { func readScripts(dir string) iter.Seq2[Script, error] {
hash := sha256.New()
return func(yield func(Script, error) bool) { return func(yield func(Script, error) bool) {
files, err := migrationsFS.ReadDir(dir) files, err := migrationsFS.ReadDir(dir)
if err != nil { if err != nil {
@ -76,11 +86,16 @@ func readScripts(dir string) iter.Seq2[Script, error] {
} }
} }
_, _ = hash.Write(content)
s := Script{ s := Script{
FileName: file.Name(), FileName: file.Name(),
Content: string(content), Content: string(content),
Hash: hash.Sum(nil),
} }
hash.Reset()
if !yield(s, nil) { if !yield(s, nil) {
return return
} }

View file

@ -1,19 +0,0 @@
-- migrate:up
-- demote postgres user
GRANT ALL ON DATABASE postgres TO postgres;
GRANT ALL ON SCHEMA auth TO postgres;
GRANT ALL ON SCHEMA extensions TO postgres;
GRANT ALL ON SCHEMA storage TO postgres;
GRANT ALL ON ALL TABLES IN SCHEMA auth TO postgres;
GRANT ALL ON ALL TABLES IN SCHEMA storage TO postgres;
GRANT ALL ON ALL TABLES IN SCHEMA extensions TO postgres;
GRANT ALL ON ALL SEQUENCES IN SCHEMA auth TO postgres;
GRANT ALL ON ALL SEQUENCES IN SCHEMA storage TO postgres;
GRANT ALL ON ALL SEQUENCES IN SCHEMA extensions TO postgres;
GRANT ALL ON ALL ROUTINES IN SCHEMA auth TO postgres;
GRANT ALL ON ALL ROUTINES IN SCHEMA storage TO postgres;
GRANT ALL ON ALL ROUTINES IN SCHEMA extensions TO postgres;
ALTER ROLE postgres NOSUPERUSER CREATEDB CREATEROLE LOGIN REPLICATION BYPASSRLS;
-- migrate:down

View file

@ -5,34 +5,44 @@ DECLARE
pgsodium_exists boolean; pgsodium_exists boolean;
vault_exists boolean; vault_exists boolean;
BEGIN BEGIN
pgsodium_exists = ( IF EXISTS (SELECT FROM pg_available_extensions WHERE name = 'supabase_vault' AND default_version != '0.2.8') THEN
select count(*) = 1 CREATE EXTENSION IF NOT EXISTS supabase_vault;
from pg_available_extensions
where name = 'pgsodium'
and default_version in ('3.1.6', '3.1.7', '3.1.8', '3.1.9')
);
vault_exists = ( -- for some reason extension custom scripts aren't run during AMI build, so
-- we manually run it here
GRANT USAGE ON SCHEMA vault TO postgres WITH GRANT OPTION;
GRANT SELECT, DELETE ON vault.secrets, vault.decrypted_secrets TO postgres WITH GRANT OPTION;
GRANT EXECUTE ON FUNCTION vault.create_secret, vault.update_secret, vault._crypto_aead_det_decrypt TO postgres WITH GRANT OPTION;
ELSE
pgsodium_exists = (
select count(*) = 1 select count(*) = 1
from pg_available_extensions from pg_available_extensions
where name = 'supabase_vault' where name = 'pgsodium'
); and default_version in ('3.1.6', '3.1.7', '3.1.8', '3.1.9')
);
IF pgsodium_exists vault_exists = (
THEN select count(*) = 1
create extension if not exists pgsodium; from pg_available_extensions
where name = 'supabase_vault'
);
grant pgsodium_keyiduser to postgres with admin option; IF pgsodium_exists
grant pgsodium_keyholder to postgres with admin option;
grant pgsodium_keymaker to postgres with admin option;
grant execute on function pgsodium.crypto_aead_det_decrypt(bytea, bytea, uuid, bytea) to service_role;
grant execute on function pgsodium.crypto_aead_det_encrypt(bytea, bytea, uuid, bytea) to service_role;
grant execute on function pgsodium.crypto_aead_det_keygen to service_role;
IF vault_exists
THEN THEN
create extension if not exists supabase_vault; create extension if not exists pgsodium;
grant pgsodium_keyiduser to postgres with admin option;
grant pgsodium_keyholder to postgres with admin option;
grant pgsodium_keymaker to postgres with admin option;
grant execute on function pgsodium.crypto_aead_det_decrypt(bytea, bytea, uuid, bytea) to service_role;
grant execute on function pgsodium.crypto_aead_det_encrypt(bytea, bytea, uuid, bytea) to service_role;
grant execute on function pgsodium.crypto_aead_det_keygen to service_role;
IF vault_exists
THEN
create extension if not exists supabase_vault;
END IF;
END IF; END IF;
END IF; END IF;
END $$; END $$;

View file

@ -0,0 +1,17 @@
-- migrate:up
DO $$
BEGIN
IF EXISTS (select from pg_available_extensions where name = 'supabase_vault')
THEN
create extension if not exists supabase_vault;
-- for some reason extension custom scripts aren't run during AMI build, so
-- we manually run it here
grant usage on schema vault to postgres with grant option;
grant select, delete on vault.secrets, vault.decrypted_secrets to postgres with grant option;
grant execute on function vault.create_secret, vault.update_secret, vault._crypto_aead_det_decrypt to postgres with grant option;
END IF;
END $$;
-- migrate:down

View file

@ -4,7 +4,12 @@ ALTER ROLE authenticated inherit;
ALTER ROLE anon inherit; ALTER ROLE anon inherit;
ALTER ROLE service_role inherit; ALTER ROLE service_role inherit;
GRANT pgsodium_keyholder to service_role; DO $$
BEGIN
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'pgsodium_keyholder') THEN
GRANT pgsodium_keyholder to service_role;
END IF;
END $$;
-- migrate:down -- migrate:down

View file

@ -0,0 +1,6 @@
-- migrate:up
alter role supabase_admin set log_statement = none;
alter role supabase_auth_admin set log_statement = none;
alter role supabase_storage_admin set log_statement = none;
-- migrate:down

View file

@ -0,0 +1,26 @@
-- migrate:up
do $$
declare
ext_schema text;
extensions_schema_exists boolean;
begin
-- check if the "extensions" schema exists
select exists (
select 1 from pg_namespace where nspname = 'extensions'
) into extensions_schema_exists;
if extensions_schema_exists then
-- check if the "orioledb" extension is in the "public" schema
select nspname into ext_schema
from pg_extension e
join pg_namespace n on e.extnamespace = n.oid
where extname = 'orioledb';
if ext_schema = 'public' then
execute 'alter extension orioledb set schema extensions';
end if;
end if;
end $$;
-- migrate:down

View file

@ -0,0 +1,31 @@
-- migrate:up
DO $$
BEGIN
IF EXISTS (SELECT FROM pg_extension WHERE extname = 'pgsodium') THEN
CREATE OR REPLACE FUNCTION pgsodium.mask_role(masked_role regrole, source_name text, view_name text)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO ''
AS $function$
BEGIN
EXECUTE format(
'GRANT SELECT ON pgsodium.key TO %s',
masked_role);
EXECUTE format(
'GRANT pgsodium_keyiduser, pgsodium_keyholder TO %s',
masked_role);
EXECUTE format(
'GRANT ALL ON %I TO %s',
view_name,
masked_role);
RETURN;
END
$function$;
END IF;
END $$;
-- migrate:down

View file

@ -0,0 +1,64 @@
-- migrate:up
CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_event_trigger_ddl_commands() AS ev
JOIN pg_extension AS ext
ON ev.objid = ext.oid
WHERE ext.extname = 'pg_net'
)
THEN
IF NOT EXISTS (
SELECT 1
FROM pg_roles
WHERE rolname = 'supabase_functions_admin'
)
THEN
CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;
END IF;
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
IF EXISTS (
SELECT FROM pg_extension
WHERE extname = 'pg_net'
-- all versions in use on existing projects as of 2025-02-20
-- version 0.12.0 onwards don't need these applied
AND extversion IN ('0.2', '0.6', '0.7', '0.7.1', '0.8', '0.10.0', '0.11.0')
) THEN
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
END IF;
END IF;
END;
$$;
DO $$
BEGIN
IF EXISTS (SELECT FROM pg_extension WHERE extname = 'pg_net')
THEN
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY INVOKER;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY INVOKER;
REVOKE EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM supabase_functions_admin, postgres, anon, authenticated, service_role;
REVOKE EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM supabase_functions_admin, postgres, anon, authenticated, service_role;
GRANT ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO PUBLIC;
GRANT ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO PUBLIC;
END IF;
END $$;
-- migrate:down

View file

@ -0,0 +1,3 @@
create schema if not exists _realtime;
alter schema _realtime owner to supabase_admin;

View file

@ -19,10 +19,13 @@ package main
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"crypto/x509"
"fmt" "fmt"
"net" "net"
"strings"
"time" "time"
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
clusterservice "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3" clusterservice "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3"
discoverygrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" discoverygrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
endpointservice "github.com/envoyproxy/go-control-plane/envoy/service/endpoint/v3" endpointservice "github.com/envoyproxy/go-control-plane/envoy/service/endpoint/v3"
@ -31,31 +34,53 @@ import (
runtimeservice "github.com/envoyproxy/go-control-plane/envoy/service/runtime/v3" runtimeservice "github.com/envoyproxy/go-control-plane/envoy/service/runtime/v3"
secretservice "github.com/envoyproxy/go-control-plane/envoy/service/secret/v3" secretservice "github.com/envoyproxy/go-control-plane/envoy/service/secret/v3"
cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
"github.com/envoyproxy/go-control-plane/pkg/log"
"github.com/envoyproxy/go-control-plane/pkg/server/v3" "github.com/envoyproxy/go-control-plane/pkg/server/v3"
"google.golang.org/grpc/credentials"
"github.com/go-logr/logr"
"google.golang.org/grpc" "google.golang.org/grpc"
grpchealth "google.golang.org/grpc/health" grpchealth "google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/keepalive" "google.golang.org/grpc/keepalive"
"google.golang.org/grpc/reflection" "google.golang.org/grpc/reflection"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/healthz"
mgr "sigs.k8s.io/controller-runtime/pkg/manager" mgr "sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters" "sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"code.icb4dc0.de/prskr/supabase-operator/internal/certs"
"code.icb4dc0.de/prskr/supabase-operator/internal/controlplane" "code.icb4dc0.de/prskr/supabase-operator/internal/controlplane"
"code.icb4dc0.de/prskr/supabase-operator/internal/health"
) )
//nolint:lll // flag declaration with struct tags is as long as it is //nolint:lll // flag declaration with struct tags is as long as it is
type controlPlane struct { type controlPlane struct {
ListenAddr string `name:"listen-address" default:":18000" help:"The address the control plane binds to."` caCert tls.Certificate `kong:"-"`
ListenAddr string `name:"listen-address" default:":18000" help:"The address the control plane binds to."`
Tls struct {
CA struct {
Cert FileContent `env:"CERT" name:"server-cert" required:"" help:"The path to the server certificate file."`
Key FileContent `env:"KEY" name:"server-key" required:"" help:"The path to the server key file."`
} `embed:"" prefix:"ca." envprefix:"CA_"`
ServerSecretName string `name:"server-secret-name" help:"The name of the secret containing the server certificate and key." default:"control-plane-xds-tls"`
} `embed:"" prefix:"tls." envprefix:"TLS_"`
MetricsAddr string `name:"metrics-bind-address" default:"0" help:"The address the metrics endpoint binds to. Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service."` MetricsAddr string `name:"metrics-bind-address" default:"0" help:"The address the metrics endpoint binds to. Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service."`
EnableLeaderElection bool `name:"leader-elect" default:"false" help:"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager."` EnableLeaderElection bool `name:"leader-elect" default:"false" help:"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager."`
ProbeAddr string `name:"health-probe-bind-address" default:":8081" help:"The address the probe endpoint binds to."` ProbeAddr string `name:"health-probe-bind-address" default:":8081" help:"The address the probe endpoint binds to."`
SecureMetrics bool `name:"metrics-secure" default:"true" help:"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead."` SecureMetrics bool `name:"metrics-secure" default:"true" help:"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead."`
EnableHTTP2 bool `name:"enable-http2" default:"false" help:"If set, HTTP/2 will be enabled for the metrics and webhook servers"` EnableHTTP2 bool `name:"enable-http2" default:"false" help:"If set, HTTP/2 will be enabled for the metrics and webhook servers"`
ServiceName string `name:"service-name" env:"CONTROL_PLANE_SERVICE_NAME" default:"" required:"" help:"The name of the control plane service."`
Namespace string `name:"namespace" env:"CONTROL_PLANE_NAMESPACE" default:"" required:"" help:"Namespace where the controller is running, ideally set via downward API"`
} }
func (cp controlPlane) Run(ctx context.Context) error { func (cp *controlPlane) Run(ctx context.Context, logger logr.Logger) error {
var tlsOpts []func(*tls.Config) var tlsOpts []func(*tls.Config)
// if the enable-http2 flag is false (the default), http/2 should be disabled // if the enable-http2 flag is false (the default), http/2 should be disabled
@ -91,6 +116,11 @@ func (cp controlPlane) Run(ctx context.Context) error {
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
} }
bootstrapClient, err := client.New(ctrl.GetConfigOrDie(), client.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("unable to create bootstrap client: %w", err)
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme, Scheme: scheme,
Metrics: metricsServerOptions, Metrics: metricsServerOptions,
@ -104,9 +134,15 @@ func (cp controlPlane) Run(ctx context.Context) error {
return fmt.Errorf("unable to start control plane: %w", err) return fmt.Errorf("unable to start control plane: %w", err)
} }
envoySnapshotCache := cachev3.NewSnapshotCache(false, cachev3.IDHash{}, nil) cacheLoggerInst := cacheLogger(logger.WithName("envoy-snapshot-cache"))
envoySnapshotCache := cachev3.NewSnapshotCache(false, cachev3.IDHash{}, cacheLoggerInst)
envoySrv, err := cp.envoyServer(ctx, envoySnapshotCache) serverCert, err := cp.ensureControlPlaneTlsCert(ctx, bootstrapClient)
if err != nil {
return fmt.Errorf("failed to ensure control plane TLS cert: %w", err)
}
envoySrv, err := cp.envoyServer(ctx, logger, envoySnapshotCache, serverCert)
if err != nil { if err != nil {
return err return err
} }
@ -123,6 +159,18 @@ func (cp controlPlane) Run(ctx context.Context) error {
return fmt.Errorf("unable to create controller Core DB: %w", err) return fmt.Errorf("unable to create controller Core DB: %w", err)
} }
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
return fmt.Errorf("unable to set up health check: %w", err)
}
if err := mgr.AddHealthzCheck("server-cert", health.CertValidCheck(serverCert)); err != nil {
return fmt.Errorf("unable to set up health check: %w", err)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
return fmt.Errorf("unable to set up ready check: %w", err)
}
setupLog.Info("starting manager") setupLog.Info("starting manager")
if err := mgr.Start(ctx); err != nil { if err := mgr.Start(ctx); err != nil {
return fmt.Errorf("problem running manager: %w", err) return fmt.Errorf("problem running manager: %w", err)
@ -131,9 +179,20 @@ func (cp controlPlane) Run(ctx context.Context) error {
return nil return nil
} }
func (cp controlPlane) envoyServer( func (cp *controlPlane) AfterApply() (err error) {
cp.caCert, err = tls.X509KeyPair(cp.Tls.CA.Cert, cp.Tls.CA.Key)
if err != nil {
return fmt.Errorf("failed to parse server certificate: %w", err)
}
return nil
}
func (cp *controlPlane) envoyServer(
ctx context.Context, ctx context.Context,
logger logr.Logger,
cache cachev3.SnapshotCache, cache cachev3.SnapshotCache,
serverCert tls.Certificate,
) (runnable mgr.Runnable, err error) { ) (runnable mgr.Runnable, err error) {
const ( const (
grpcKeepaliveTime = 30 * time.Second grpcKeepaliveTime = 30 * time.Second
@ -141,11 +200,8 @@ func (cp controlPlane) envoyServer(
grpcKeepaliveMinTime = 30 * time.Second grpcKeepaliveMinTime = 30 * time.Second
grpcMaxConcurrentStreams = 1000000 grpcMaxConcurrentStreams = 1000000
) )
srv := server.NewServer(ctx, cache, xdsServerCallbacks(logger))
var ( logger = logger.WithName("control-plane")
logger = ctrl.Log.WithName("control-plane")
srv = server.NewServer(ctx, cache, nil)
)
// gRPC golang library sets a very small upper bound for the number gRPC/h2 // gRPC golang library sets a very small upper bound for the number gRPC/h2
// streams over a single TCP connection. If a proxy multiplexes requests over // streams over a single TCP connection. If a proxy multiplexes requests over
@ -153,7 +209,13 @@ func (cp controlPlane) envoyServer(
// availability problems. Keepalive timeouts based on connection_keepalive parameter // availability problems. Keepalive timeouts based on connection_keepalive parameter
// https://www.envoyproxy.io/docs/envoy/latest/configuration/overview/examples#dynamic // https://www.envoyproxy.io/docs/envoy/latest/configuration/overview/examples#dynamic
tlsCfg, err := cp.tlsConfig(serverCert)
if err != nil {
return nil, fmt.Errorf("failed to create TLS config: %w", err)
}
grpcOptions := append(make([]grpc.ServerOption, 0, 4), grpcOptions := append(make([]grpc.ServerOption, 0, 4),
grpc.Creds(credentials.NewTLS(tlsCfg)),
grpc.MaxConcurrentStreams(grpcMaxConcurrentStreams), grpc.MaxConcurrentStreams(grpcMaxConcurrentStreams),
grpc.KeepaliveParams(keepalive.ServerParameters{ grpc.KeepaliveParams(keepalive.ServerParameters{
Time: grpcKeepaliveTime, Time: grpcKeepaliveTime,
@ -195,3 +257,169 @@ func (cp controlPlane) envoyServer(
return grpcServer.Serve(lis) return grpcServer.Serve(lis)
}), nil }), nil
} }
func (cp *controlPlane) ensureControlPlaneTlsCert(
ctx context.Context,
k8sClient client.Client,
) (tls.Certificate, error) {
var (
controlPlaneServerCert = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: cp.Tls.ServerSecretName,
Namespace: cp.Namespace,
},
}
serverCert tls.Certificate
)
_, err := controllerutil.CreateOrUpdate(ctx, k8sClient, controlPlaneServerCert, func() (err error) {
controlPlaneServerCert.Type = corev1.SecretTypeTLS
if controlPlaneServerCert.Data == nil {
controlPlaneServerCert.Data = make(map[string][]byte, 3)
}
var (
cert = controlPlaneServerCert.Data[corev1.TLSCertKey]
privateKey = controlPlaneServerCert.Data[corev1.TLSPrivateKeyKey]
)
var requireRenewal bool
if cert != nil && privateKey != nil {
if serverCert, err = tls.X509KeyPair(cert, privateKey); err != nil {
return fmt.Errorf("failed to parse server certificate: %w", err)
}
renewGracePeriod := time.Duration(float64(serverCert.Leaf.NotAfter.Sub(serverCert.Leaf.NotBefore)) * 0.1)
if serverCert.Leaf.NotAfter.Before(time.Now().Add(-renewGracePeriod)) {
requireRenewal = true
}
} else {
requireRenewal = true
}
if requireRenewal {
dnsNames := []string{
strings.Join([]string{cp.ServiceName, cp.Namespace, "svc"}, "."),
strings.Join([]string{cp.ServiceName, cp.Namespace, "svc", "cluster", "local"}, "."),
}
if certResult, err := certs.ServerCert("supabase-control-plane", dnsNames, cp.caCert); err != nil {
return fmt.Errorf("failed to generate server certificate: %w", err)
} else {
serverCert = certResult.ServerCert
controlPlaneServerCert.Data[corev1.TLSCertKey] = certResult.PublicKey
controlPlaneServerCert.Data[corev1.TLSPrivateKeyKey] = certResult.PrivateKey
}
}
return nil
})
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to create or update control plane server certificate: %w", err)
}
return serverCert, nil
}
func (cp *controlPlane) tlsConfig(serverCert tls.Certificate) (*tls.Config, error) {
tlsCfg := &tls.Config{
RootCAs: x509.NewCertPool(),
ClientCAs: x509.NewCertPool(),
ClientAuth: tls.RequireAndVerifyClientCert,
}
tlsCfg.Certificates = append(tlsCfg.Certificates, serverCert)
if !tlsCfg.RootCAs.AppendCertsFromPEM(cp.Tls.CA.Cert) {
return nil, fmt.Errorf("failed to parse CA certificate")
}
if !tlsCfg.ClientCAs.AppendCertsFromPEM(cp.Tls.CA.Cert) {
return nil, fmt.Errorf("failed to parse client CA certificate")
}
return tlsCfg, nil
}
func xdsServerCallbacks(logger logr.Logger) server.Callbacks {
return server.CallbackFuncs{
StreamOpenFunc: func(ctx context.Context, streamId int64, nodeId string) error {
logger.Info("Stream opened", "stream-id", streamId, "node-id", nodeId)
return nil
},
StreamClosedFunc: func(streamId int64, node *corev3.Node) {
logger.Info("Stream closed", "stream-id", streamId,
"node.id", node.Id,
"node.cluster", node.Cluster,
)
},
StreamRequestFunc: func(streamId int64, request *discoverygrpc.DiscoveryRequest) error {
logger.Info("Stream request",
"stream-id", streamId,
"request.node.id", request.Node.Id,
"request.node.cluster", request.Node.Cluster,
"request.version", request.VersionInfo,
"request.error", request.ErrorDetail,
)
return nil
},
StreamResponseFunc: func(
ctx context.Context,
streamId int64,
request *discoverygrpc.DiscoveryRequest,
response *discoverygrpc.DiscoveryResponse,
) {
logger.Info("Stream delta response",
"stream-id", streamId,
"request.node.id", request.Node.Id,
"request.node.cluster", request.Node.Cluster,
)
},
DeltaStreamOpenFunc: func(ctx context.Context, streamId int64, nodeId string) error {
logger.Info("Delta stream opened", "stream-id", streamId, "node-id", nodeId)
return nil
},
DeltaStreamClosedFunc: func(streamId int64, node *corev3.Node) {
logger.Info("Delta stream closed",
"stream-id", streamId,
"node.id", node.Id,
"node.cluster", node.Cluster,
)
},
StreamDeltaRequestFunc: func(i int64, request *discoverygrpc.DeltaDiscoveryRequest) error {
logger.Info("Stream delta request",
"stream-id", i,
"request.node.id", request.Node.Id,
"request.node.cluster", request.Node.Cluster,
"request.error", request.ErrorDetail,
)
return nil
},
StreamDeltaResponseFunc: func(
i int64,
request *discoverygrpc.DeltaDiscoveryRequest,
response *discoverygrpc.DeltaDiscoveryResponse,
) {
logger.Info("Stream delta response",
"stream-id", i,
"request.node", request.Node,
"response.resources", response.Resources,
)
},
}
}
func cacheLogger(logger logr.Logger) log.Logger {
wrapper := func(delegate func(msg string, keysAndValues ...any)) func(string, ...any) {
return func(s string, i ...any) {
delegate(fmt.Sprintf(s, i...))
}
}
return log.LoggerFuncs{
DebugFunc: nil, // enable for debug info
InfoFunc: wrapper(logger.Info),
WarnFunc: wrapper(logger.Info),
ErrorFunc: wrapper(logger.Info),
}
}

41
cmd/flags.go Normal file
View file

@ -0,0 +1,41 @@
/*
Copyright 2025 Peter Kurfer.
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 (
"fmt"
"os"
"github.com/alecthomas/kong"
)
var _ kong.MapperValue = (*FileContent)(nil)
type FileContent []byte
func (f *FileContent) Decode(ctx *kong.DecodeContext) (err error) {
var filePath string
if err := ctx.Scan.PopValueInto("file-content", &filePath); err != nil {
return err
}
if *f, err = os.ReadFile(filePath); err != nil {
return fmt.Errorf("failed to read file: %w", err)
}
return nil
}

View file

@ -40,6 +40,10 @@ type manager struct {
SecureMetrics bool `name:"metrics-secure" default:"true" help:"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead."` SecureMetrics bool `name:"metrics-secure" default:"true" help:"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead."`
EnableHTTP2 bool `name:"enable-http2" default:"false" help:"If set, HTTP/2 will be enabled for the metrics and webhook servers"` EnableHTTP2 bool `name:"enable-http2" default:"false" help:"If set, HTTP/2 will be enabled for the metrics and webhook servers"`
Namespace string `name:"controller-namespace" env:"CONTROLLER_NAMESPACE" default:"" help:"Namespace where the controller is running, ideally set via downward API"` Namespace string `name:"controller-namespace" env:"CONTROLLER_NAMESPACE" default:"" help:"Namespace where the controller is running, ideally set via downward API"`
Tls struct {
CACert FileContent `env:"CA_CERT" name:"ca-cert" required:"" help:"The path to the CA certificate file."`
CAKey FileContent `env:"CA_KEY" name:"ca-key" required:"" help:"The path to the CA key file."`
} `embed:"" prefix:"tls." envprefix:"TLS_"`
} }
func (m manager) Run(ctx context.Context) error { func (m manager) Run(ctx context.Context) error {
@ -68,6 +72,11 @@ func (m manager) Run(ctx context.Context) error {
TLSOpts: tlsOpts, TLSOpts: tlsOpts,
}) })
caCert, err := tls.X509KeyPair(m.Tls.CACert, m.Tls.CAKey)
if err != nil {
return fmt.Errorf("unable to load CA cert: %w", err)
}
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
// More info: // More info:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
@ -145,6 +154,7 @@ func (m manager) Run(ctx context.Context) error {
if err = (&controller.APIGatewayReconciler{ if err = (&controller.APIGatewayReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
CACert: caCert,
}).SetupWithManager(ctx, mgr); err != nil { }).SetupWithManager(ctx, mgr); err != nil {
return fmt.Errorf("unable to create controller APIGateway: %w", err) return fmt.Errorf("unable to create controller APIGateway: %w", err)
} }

View file

@ -0,0 +1,33 @@
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: cp-selfsigned-issuer
namespace: system
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
labels:
app.kubernetes.io/name: certificate
app.kubernetes.io/instance: serving-cert
app.kubernetes.io/component: certificate
app.kubernetes.io/created-by: supabase-operator
app.kubernetes.io/part-of: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: cp-ca-cert
namespace: supabase-system
spec:
commonName: control-plane-ca
privateKey:
algorithm: ECDSA
issuerRef:
kind: Issuer
name: selfsigned-issuer
secretName: control-plane-ca-cert-tls
isCA: true

View file

@ -18,40 +18,26 @@ spec:
labels: labels:
app.kubernetes.io/name: control-plane app.kubernetes.io/name: control-plane
spec: 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: securityContext:
runAsNonRoot: true runAsNonRoot: true
# TODO(user): For common cases that do not require escalating privileges seccompProfile:
# it is recommended to ensure that all your Pods/Containers are restrictive. type: RuntimeDefault
# More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
# Please uncomment the following code if your project does NOT have to work on old Kubernetes
# versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ).
# seccompProfile:
# type: RuntimeDefault
containers: containers:
- args: - args:
- control-plane - control-plane
image: supabase-operator:latest image: controller:latest
name: control-plane name: control-plane
env:
- name: CONTROL_PLANE_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: CONTROL_PLANE_SERVICE_NAME
value: control-plane
- name: TLS_CA_CERT
value: /etc/supabase/control-plane/certs/tls.crt
- name: TLS_CA_KEY
value: /etc/supabase/control-plane/certs/tls.key
ports: ports:
- containerPort: 18000 - containerPort: 18000
name: grpc name: grpc
@ -62,17 +48,17 @@ spec:
drop: drop:
- "ALL" - "ALL"
livenessProbe: livenessProbe:
grpc: httpGet:
port: 18000 path: /healthz
port: 8081
initialDelaySeconds: 15 initialDelaySeconds: 15
periodSeconds: 20 periodSeconds: 20
readinessProbe: readinessProbe:
grpc: httpGet:
port: 18000 path: /readyz
port: 8081
initialDelaySeconds: 5 initialDelaySeconds: 5
periodSeconds: 10 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: resources:
limits: limits:
cpu: 150m cpu: 150m
@ -80,5 +66,12 @@ spec:
requests: requests:
cpu: 50m cpu: 50m
memory: 64Mi memory: 64Mi
volumeMounts:
- name: tls-certs
mountPath: /etc/supabase/control-plane/certs
volumes:
- name: tls-certs
secret:
secretName: control-plane-ca-cert-tls
serviceAccountName: control-plane serviceAccountName: control-plane
terminationGracePeriodSeconds: 10 terminationGracePeriodSeconds: 10

View file

@ -2,5 +2,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
- cert-ca.yaml
- control-plane.yaml - control-plane.yaml
- service.yaml - service.yaml
configurations:
- kustomizeconfig.yaml

View file

@ -0,0 +1,8 @@
# This configuration is for teaching kustomize how to update name ref substitution
nameReference:
- kind: Issuer
group: cert-manager.io
fieldSpecs:
- kind: Certificate
group: cert-manager.io
path: spec/issuerRef/name

View file

@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.17.1 controller-gen.kubebuilder.io/version: v0.17.2
name: apigateways.supabase.k8s.icb4dc0.de name: apigateways.supabase.k8s.icb4dc0.de
spec: spec:
group: supabase.k8s.icb4dc0.de group: supabase.k8s.icb4dc0.de
@ -15,7 +15,7 @@ spec:
scope: Namespaced scope: Namespaced
versions: versions:
- additionalPrinterColumns: - additionalPrinterColumns:
- jsonPath: .status.envoy.configVersion - jsonPath: .status.envoy.resourceHash
name: EnvoyConfigVersion name: EnvoyConfigVersion
type: string type: string
name: v1alpha1 name: v1alpha1
@ -43,11 +43,198 @@ spec:
spec: spec:
description: APIGatewaySpec defines the desired state of APIGateway. description: APIGatewaySpec defines the desired state of APIGateway.
properties: properties:
apiEndpoint:
description: |-
ApiEndpoint - Configure the endpoint for all API routes
this includes the JWT configuration
properties:
jwks:
description: JWKSSelector - selector where the JWKS can be retrieved
from to enable the API gateway to validate JWTs
properties:
key:
description: The key of the secret to select from. Must be
a valid secret key.
type: string
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
optional:
description: Specify whether the Secret or its key must be
defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
tls:
description: TLS - enable and configure TLS for the API endpoint
properties:
cert:
properties:
caCertKey:
default: ca.crt
description: CaCertKey - key in the secret that contains
the CA certificate
type: string
secretName:
type: string
serverCertKey:
default: tls.crt
description: ServerCertKey - key in the secret that contains
the server certificate
type: string
serverKeyKey:
default: tls.key
description: ServerKeyKey - key in the secret that contains
the server private key
type: string
required:
- secretName
- serverCertKey
- serverKeyKey
type: object
required:
- cert
type: object
required:
- jwks
type: object
componentTypeLabel: componentTypeLabel:
default: app.kubernetes.io/name default: app.kubernetes.io/name
description: ComponentTypeLabel - Label to identify which Supabase description: ComponentTypeLabel - Label to identify which Supabase
component a Service represents (e.g. auth, postgrest, ...) component a Service represents (e.g. auth, postgrest, ...)
type: string type: string
dashboardEndpoint:
description: |-
DashboardEndpoint - Configure the endpoint for the Supabase dashboard (studio)
this includes optional authentication (basic or Oauth2) for the dashboard
properties:
auth:
description: Auth - configure authentication for the dashboard
endpoint
properties:
basic:
description: |-
Basic - HTTP basic auth configuration, this should only be used in exceptions
e.g. during evaluations or for local development
only used if no other authentication is configured
properties:
plaintextUsersSecretRef:
description: |-
PlaintextUsersSecretRef - name of a secret that contains plaintext credentials in key-value form
if not empty, credentials will be merged with inline users
type: string
usersInline:
description: UsersInline - [htpasswd format](https://httpd.apache.org/docs/2.4/programs/htpasswd.html)
items:
pattern: ^[\w_.]+:\{SHA\}[A-z0-9]+=*$
type: string
type: array
type: object
oauth2:
description: |-
OAuth2 - configure oauth2 authentication for the dashhboard listener
if configured, will be preferred over Basic authentication configuration
effectively disabling basic auth
properties:
authorizationEndpoint:
description: AuthorizationEndpoint - endpoint where the
user will be redirected to authenticate
type: string
clientId:
description: ClientID - client ID to authenticate with
the OAuth2 provider
type: string
clientSecretRef:
description: ClientSecretRef - reference to the secret
that contains the client secret
properties:
key:
description: The key of the secret to select from. Must
be a valid secret key.
type: string
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
optional:
description: Specify whether the Secret or its key
must be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
openIdIssuer:
description: |-
OpenIDIssuer - if set the defaulter will fetch the discovery document and fill
TokenEndpoint and AuthorizationEndpoint based on the discovery document
type: string
resources:
description: Resources - resources to request from the
OAuth2 provider (e.g. "user", "email", ...) - optional
items:
type: string
type: array
scopes:
description: Scopes - scopes to request from the OAuth2
provider (e.g. "openid", "profile", ...) - optional
items:
type: string
type: array
tokenEndpoint:
description: TokenEndpoint - endpoint where Envoy will
retrieve the OAuth2 access and identity token from
type: string
required:
- clientId
- clientSecretRef
type: object
type: object
tls:
description: TLS - enable and configure TLS for the Dashboard
endpoint
properties:
cert:
properties:
caCertKey:
default: ca.crt
description: CaCertKey - key in the secret that contains
the CA certificate
type: string
secretName:
type: string
serverCertKey:
default: tls.crt
description: ServerCertKey - key in the secret that contains
the server certificate
type: string
serverKeyKey:
default: tls.key
description: ServerKeyKey - key in the secret that contains
the server private key
type: string
required:
- secretName
- serverCertKey
- serverKeyKey
type: object
required:
- cert
type: object
type: object
envoy: envoy:
description: Envoy - configure the envoy instance and most importantly description: Envoy - configure the envoy instance and most importantly
the control-plane the control-plane
@ -70,13 +257,45 @@ spec:
- host - host
- port - port
type: object type: object
debugging:
properties:
componentLogLevels:
items:
properties:
component:
description: |-
Component - the component to set the log level for
the component IDs can be found [here](https://github.com/envoyproxy/envoy/blob/main/source/common/common/logger.h#L36)
type: string
level:
description: Level - the log level to set for the component
enum:
- trace
- debug
- info
- warning
- error
- critical
- "off"
type: string
required:
- component
- level
type: object
type: array
type: object
disableIPv6:
description: |-
DisableIPv6 - disable IPv6 for the Envoy instance
this will force Envoy to use IPv4 for upstream hosts (mostly for the OAuth2 token endpoint)
type: boolean
nodeName: nodeName:
description: |- description: |-
NodeName - identifies the Envoy cluster within the current namespace NodeName - identifies the Envoy cluster within the current namespace
if not set, the name of the APIGateway resource will be used if not set, the name of the APIGateway resource will be used
The primary use case is to make the assignment of multiple supabase instances in a single namespace explicit. The primary use case is to make the assignment of multiple supabase instances in a single namespace explicit.
type: string type: string
workloadTemplate: workloadSpec:
description: WorkloadTemplate - customize the Envoy deployment description: WorkloadTemplate - customize the Envoy deployment
properties: properties:
additionalLabels: additionalLabels:
@ -1883,248 +2102,9 @@ spec:
- name - name
type: object type: object
type: array type: array
replicas: container:
format: int32 description: ContainerSpec - customize the container template
type: integer of the workload
securityContext:
description: |-
PodSecurityContext holds pod-level security attributes and common container settings.
Some fields are also present in container.securityContext. Field values of
container.securityContext take precedence over field values of PodSecurityContext.
properties:
appArmorProfile:
description: |-
appArmorProfile is the AppArmor options to use by the containers in this pod.
Note that this field cannot be set when spec.os.name is windows.
properties:
localhostProfile:
description: |-
localhostProfile indicates a profile loaded on the node that should be used.
The profile must be preconfigured on the node to work.
Must match the loaded name of the profile.
Must be set if and only if type is "Localhost".
type: string
type:
description: |-
type indicates which kind of AppArmor profile will be applied.
Valid options are:
Localhost - a profile pre-loaded on the node.
RuntimeDefault - the container runtime's default profile.
Unconfined - no AppArmor enforcement.
type: string
required:
- type
type: object
fsGroup:
description: |-
A special supplemental group that applies to all containers in a pod.
Some volume types allow the Kubelet to change the ownership of that volume
to be owned by the pod:
1. The owning GID will be the FSGroup
2. The setgid bit is set (new files created in the volume will be owned by FSGroup)
3. The permission bits are OR'd with rw-rw----
If unset, the Kubelet will not modify the ownership and permissions of any volume.
Note that this field cannot be set when spec.os.name is windows.
format: int64
type: integer
fsGroupChangePolicy:
description: |-
fsGroupChangePolicy defines behavior of changing ownership and permission of the volume
before being exposed inside Pod. This field will only apply to
volume types which support fsGroup based ownership(and permissions).
It will have no effect on ephemeral volume types such as: secret, configmaps
and emptydir.
Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used.
Note that this field cannot be set when spec.os.name is windows.
type: string
runAsGroup:
description: |-
The GID to run the entrypoint of the container process.
Uses runtime default if unset.
May also be set in SecurityContext. If set in both SecurityContext and
PodSecurityContext, the value specified in SecurityContext takes precedence
for that container.
Note that this field cannot be set when spec.os.name is windows.
format: int64
type: integer
runAsNonRoot:
description: |-
Indicates that the container must run as a non-root user.
If true, the Kubelet will validate the image at runtime to ensure that it
does not run as UID 0 (root) and fail to start the container if it does.
If unset or false, no such validation will be performed.
May also be set in SecurityContext. If set in both SecurityContext and
PodSecurityContext, the value specified in SecurityContext takes precedence.
type: boolean
runAsUser:
description: |-
The UID to run the entrypoint of the container process.
Defaults to user specified in image metadata if unspecified.
May also be set in SecurityContext. If set in both SecurityContext and
PodSecurityContext, the value specified in SecurityContext takes precedence
for that container.
Note that this field cannot be set when spec.os.name is windows.
format: int64
type: integer
seLinuxChangePolicy:
description: |-
seLinuxChangePolicy defines how the container's SELinux label is applied to all volumes used by the Pod.
It has no effect on nodes that do not support SELinux or to volumes does not support SELinux.
Valid values are "MountOption" and "Recursive".
"Recursive" means relabeling of all files on all Pod volumes by the container runtime.
This may be slow for large volumes, but allows mixing privileged and unprivileged Pods sharing the same volume on the same node.
"MountOption" mounts all eligible Pod volumes with `-o context` mount option.
This requires all Pods that share the same volume to use the same SELinux label.
It is not possible to share the same volume among privileged and unprivileged Pods.
Eligible volumes are in-tree FibreChannel and iSCSI volumes, and all CSI volumes
whose CSI driver announces SELinux support by setting spec.seLinuxMount: true in their
CSIDriver instance. Other volumes are always re-labelled recursively.
"MountOption" value is allowed only when SELinuxMount feature gate is enabled.
If not specified and SELinuxMount feature gate is enabled, "MountOption" is used.
If not specified and SELinuxMount feature gate is disabled, "MountOption" is used for ReadWriteOncePod volumes
and "Recursive" for all other volumes.
This field affects only Pods that have SELinux label set, either in PodSecurityContext or in SecurityContext of all containers.
All Pods that use the same volume should use the same seLinuxChangePolicy, otherwise some pods can get stuck in ContainerCreating state.
Note that this field cannot be set when spec.os.name is windows.
type: string
seLinuxOptions:
description: |-
The SELinux context to be applied to all containers.
If unspecified, the container runtime will allocate a random SELinux context for each
container. May also be set in SecurityContext. If set in
both SecurityContext and PodSecurityContext, the value specified in SecurityContext
takes precedence for that container.
Note that this field cannot be set when spec.os.name is windows.
properties:
level:
description: Level is SELinux level label that applies
to the container.
type: string
role:
description: Role is a SELinux role label that applies
to the container.
type: string
type:
description: Type is a SELinux type label that applies
to the container.
type: string
user:
description: User is a SELinux user label that applies
to the container.
type: string
type: object
seccompProfile:
description: |-
The seccomp options to use by the containers in this pod.
Note that this field cannot be set when spec.os.name is windows.
properties:
localhostProfile:
description: |-
localhostProfile indicates a profile defined in a file on the node should be used.
The profile must be preconfigured on the node to work.
Must be a descending path, relative to the kubelet's configured seccomp profile location.
Must be set if type is "Localhost". Must NOT be set for any other type.
type: string
type:
description: |-
type indicates which kind of seccomp profile will be applied.
Valid options are:
Localhost - a profile defined in a file on the node should be used.
RuntimeDefault - the container runtime default profile should be used.
Unconfined - no profile should be applied.
type: string
required:
- type
type: object
supplementalGroups:
description: |-
A list of groups applied to the first process run in each container, in
addition to the container's primary GID and fsGroup (if specified). If
the SupplementalGroupsPolicy feature is enabled, the
supplementalGroupsPolicy field determines whether these are in addition
to or instead of any group memberships defined in the container image.
If unspecified, no additional groups are added, though group memberships
defined in the container image may still be used, depending on the
supplementalGroupsPolicy field.
Note that this field cannot be set when spec.os.name is windows.
items:
format: int64
type: integer
type: array
x-kubernetes-list-type: atomic
supplementalGroupsPolicy:
description: |-
Defines how supplemental groups of the first container processes are calculated.
Valid values are "Merge" and "Strict". If not specified, "Merge" is used.
(Alpha) Using the field requires the SupplementalGroupsPolicy feature gate to be enabled
and the container runtime must implement support for this feature.
Note that this field cannot be set when spec.os.name is windows.
type: string
sysctls:
description: |-
Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported
sysctls (by the container runtime) might fail to launch.
Note that this field cannot be set when spec.os.name is windows.
items:
description: Sysctl defines a kernel parameter to be
set
properties:
name:
description: Name of a property to set
type: string
value:
description: Value of a property to set
type: string
required:
- name
- value
type: object
type: array
x-kubernetes-list-type: atomic
windowsOptions:
description: |-
The Windows specific settings applied to all containers.
If unspecified, the options within a container's SecurityContext will be used.
If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
Note that this field cannot be set when spec.os.name is linux.
properties:
gmsaCredentialSpec:
description: |-
GMSACredentialSpec is where the GMSA admission webhook
(https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the
GMSA credential spec named by the GMSACredentialSpecName field.
type: string
gmsaCredentialSpecName:
description: GMSACredentialSpecName is the name of
the GMSA credential spec to use.
type: string
hostProcess:
description: |-
HostProcess determines if a container should be run as a 'Host Process' container.
All of a Pod's containers must have the same effective HostProcess value
(it is not allowed to have a mix of HostProcess containers and non-HostProcess containers).
In addition, if HostProcess is true then HostNetwork must also be set to true.
type: boolean
runAsUserName:
description: |-
The UserName in Windows to run the entrypoint of the container process.
Defaults to the user specified in image metadata if unspecified.
May also be set in PodSecurityContext. If set in both SecurityContext and
PodSecurityContext, the value specified in SecurityContext takes precedence.
type: string
type: object
type: object
workload:
description: Workload - customize the container template of
the workload
properties: properties:
additionalEnv: additionalEnv:
items: items:
@ -2589,34 +2569,249 @@ spec:
type: object type: object
type: array type: array
type: object type: object
replicas:
format: int32
type: integer
securityContext:
description: |-
PodSecurityContext holds pod-level security attributes and common container settings.
Some fields are also present in container.securityContext. Field values of
container.securityContext take precedence over field values of PodSecurityContext.
properties:
appArmorProfile:
description: |-
appArmorProfile is the AppArmor options to use by the containers in this pod.
Note that this field cannot be set when spec.os.name is windows.
properties:
localhostProfile:
description: |-
localhostProfile indicates a profile loaded on the node that should be used.
The profile must be preconfigured on the node to work.
Must match the loaded name of the profile.
Must be set if and only if type is "Localhost".
type: string
type:
description: |-
type indicates which kind of AppArmor profile will be applied.
Valid options are:
Localhost - a profile pre-loaded on the node.
RuntimeDefault - the container runtime's default profile.
Unconfined - no AppArmor enforcement.
type: string
required:
- type
type: object
fsGroup:
description: |-
A special supplemental group that applies to all containers in a pod.
Some volume types allow the Kubelet to change the ownership of that volume
to be owned by the pod:
1. The owning GID will be the FSGroup
2. The setgid bit is set (new files created in the volume will be owned by FSGroup)
3. The permission bits are OR'd with rw-rw----
If unset, the Kubelet will not modify the ownership and permissions of any volume.
Note that this field cannot be set when spec.os.name is windows.
format: int64
type: integer
fsGroupChangePolicy:
description: |-
fsGroupChangePolicy defines behavior of changing ownership and permission of the volume
before being exposed inside Pod. This field will only apply to
volume types which support fsGroup based ownership(and permissions).
It will have no effect on ephemeral volume types such as: secret, configmaps
and emptydir.
Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used.
Note that this field cannot be set when spec.os.name is windows.
type: string
runAsGroup:
description: |-
The GID to run the entrypoint of the container process.
Uses runtime default if unset.
May also be set in SecurityContext. If set in both SecurityContext and
PodSecurityContext, the value specified in SecurityContext takes precedence
for that container.
Note that this field cannot be set when spec.os.name is windows.
format: int64
type: integer
runAsNonRoot:
description: |-
Indicates that the container must run as a non-root user.
If true, the Kubelet will validate the image at runtime to ensure that it
does not run as UID 0 (root) and fail to start the container if it does.
If unset or false, no such validation will be performed.
May also be set in SecurityContext. If set in both SecurityContext and
PodSecurityContext, the value specified in SecurityContext takes precedence.
type: boolean
runAsUser:
description: |-
The UID to run the entrypoint of the container process.
Defaults to user specified in image metadata if unspecified.
May also be set in SecurityContext. If set in both SecurityContext and
PodSecurityContext, the value specified in SecurityContext takes precedence
for that container.
Note that this field cannot be set when spec.os.name is windows.
format: int64
type: integer
seLinuxChangePolicy:
description: |-
seLinuxChangePolicy defines how the container's SELinux label is applied to all volumes used by the Pod.
It has no effect on nodes that do not support SELinux or to volumes does not support SELinux.
Valid values are "MountOption" and "Recursive".
"Recursive" means relabeling of all files on all Pod volumes by the container runtime.
This may be slow for large volumes, but allows mixing privileged and unprivileged Pods sharing the same volume on the same node.
"MountOption" mounts all eligible Pod volumes with `-o context` mount option.
This requires all Pods that share the same volume to use the same SELinux label.
It is not possible to share the same volume among privileged and unprivileged Pods.
Eligible volumes are in-tree FibreChannel and iSCSI volumes, and all CSI volumes
whose CSI driver announces SELinux support by setting spec.seLinuxMount: true in their
CSIDriver instance. Other volumes are always re-labelled recursively.
"MountOption" value is allowed only when SELinuxMount feature gate is enabled.
If not specified and SELinuxMount feature gate is enabled, "MountOption" is used.
If not specified and SELinuxMount feature gate is disabled, "MountOption" is used for ReadWriteOncePod volumes
and "Recursive" for all other volumes.
This field affects only Pods that have SELinux label set, either in PodSecurityContext or in SecurityContext of all containers.
All Pods that use the same volume should use the same seLinuxChangePolicy, otherwise some pods can get stuck in ContainerCreating state.
Note that this field cannot be set when spec.os.name is windows.
type: string
seLinuxOptions:
description: |-
The SELinux context to be applied to all containers.
If unspecified, the container runtime will allocate a random SELinux context for each
container. May also be set in SecurityContext. If set in
both SecurityContext and PodSecurityContext, the value specified in SecurityContext
takes precedence for that container.
Note that this field cannot be set when spec.os.name is windows.
properties:
level:
description: Level is SELinux level label that applies
to the container.
type: string
role:
description: Role is a SELinux role label that applies
to the container.
type: string
type:
description: Type is a SELinux type label that applies
to the container.
type: string
user:
description: User is a SELinux user label that applies
to the container.
type: string
type: object
seccompProfile:
description: |-
The seccomp options to use by the containers in this pod.
Note that this field cannot be set when spec.os.name is windows.
properties:
localhostProfile:
description: |-
localhostProfile indicates a profile defined in a file on the node should be used.
The profile must be preconfigured on the node to work.
Must be a descending path, relative to the kubelet's configured seccomp profile location.
Must be set if type is "Localhost". Must NOT be set for any other type.
type: string
type:
description: |-
type indicates which kind of seccomp profile will be applied.
Valid options are:
Localhost - a profile defined in a file on the node should be used.
RuntimeDefault - the container runtime default profile should be used.
Unconfined - no profile should be applied.
type: string
required:
- type
type: object
supplementalGroups:
description: |-
A list of groups applied to the first process run in each container, in
addition to the container's primary GID and fsGroup (if specified). If
the SupplementalGroupsPolicy feature is enabled, the
supplementalGroupsPolicy field determines whether these are in addition
to or instead of any group memberships defined in the container image.
If unspecified, no additional groups are added, though group memberships
defined in the container image may still be used, depending on the
supplementalGroupsPolicy field.
Note that this field cannot be set when spec.os.name is windows.
items:
format: int64
type: integer
type: array
x-kubernetes-list-type: atomic
supplementalGroupsPolicy:
description: |-
Defines how supplemental groups of the first container processes are calculated.
Valid values are "Merge" and "Strict". If not specified, "Merge" is used.
(Alpha) Using the field requires the SupplementalGroupsPolicy feature gate to be enabled
and the container runtime must implement support for this feature.
Note that this field cannot be set when spec.os.name is windows.
type: string
sysctls:
description: |-
Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported
sysctls (by the container runtime) might fail to launch.
Note that this field cannot be set when spec.os.name is windows.
items:
description: Sysctl defines a kernel parameter to be
set
properties:
name:
description: Name of a property to set
type: string
value:
description: Value of a property to set
type: string
required:
- name
- value
type: object
type: array
x-kubernetes-list-type: atomic
windowsOptions:
description: |-
The Windows specific settings applied to all containers.
If unspecified, the options within a container's SecurityContext will be used.
If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
Note that this field cannot be set when spec.os.name is linux.
properties:
gmsaCredentialSpec:
description: |-
GMSACredentialSpec is where the GMSA admission webhook
(https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the
GMSA credential spec named by the GMSACredentialSpecName field.
type: string
gmsaCredentialSpecName:
description: GMSACredentialSpecName is the name of
the GMSA credential spec to use.
type: string
hostProcess:
description: |-
HostProcess determines if a container should be run as a 'Host Process' container.
All of a Pod's containers must have the same effective HostProcess value
(it is not allowed to have a mix of HostProcess containers and non-HostProcess containers).
In addition, if HostProcess is true then HostNetwork must also be set to true.
type: boolean
runAsUserName:
description: |-
The UserName in Windows to run the entrypoint of the container process.
Defaults to the user specified in image metadata if unspecified.
May also be set in PodSecurityContext. If set in both SecurityContext and
PodSecurityContext, the value specified in SecurityContext takes precedence.
type: string
type: object
type: object
type: object type: object
required: required:
- controlPlane - controlPlane
type: object type: object
jwks:
description: JWKSSelector - selector where the JWKS can be retrieved
from to enable the API gateway to validate JWTs
properties:
key:
description: The key of the secret to select from. Must be a
valid secret key.
type: string
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
optional:
description: Specify whether the Secret or its key must be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
serviceSelector: serviceSelector:
default: default:
matchExpressions: matchExpressions:
@ -2674,7 +2869,6 @@ spec:
x-kubernetes-map-type: atomic x-kubernetes-map-type: atomic
required: required:
- envoy - envoy
- jwks
- serviceSelector - serviceSelector
type: object type: object
status: status:
@ -2682,8 +2876,6 @@ spec:
properties: properties:
envoy: envoy:
properties: properties:
configVersion:
type: string
resourceHash: resourceHash:
format: byte format: byte
type: string type: string

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -23,7 +23,6 @@ resources:
- ../certmanager - ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus #- ../prometheus
# [METRICS] Expose the controller manager metrics service.
- metrics_service.yaml - metrics_service.yaml
# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. # [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 Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics.
@ -44,19 +43,31 @@ patches:
# crd/kustomization.yaml # crd/kustomization.yaml
- path: manager_webhook_patch.yaml - path: manager_webhook_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
# Uncomment the following replacements to add the cert-manager CA injection annotations
replacements: replacements:
- source: # Uncomment the following block if you have any webhook - source:
kind: Service
version: v1
name: control-plane
fieldPath: .metadata.name
targets:
- select:
kind: Deployment
group: apps
version: v1
name: control-plane
fieldPaths:
- .spec.template.spec.containers.*.env.[name=CONTROL_PLANE_SERVICE_NAME].value
- source:
kind: Service kind: Service
version: v1 version: v1
name: webhook-service name: webhook-service
fieldPath: .metadata.name # Name of the service fieldPath: .metadata.name
targets: targets:
- select: - select:
kind: Certificate kind: Certificate
group: cert-manager.io group: cert-manager.io
version: v1 version: v1
name: serving-cert
fieldPaths: fieldPaths:
- .spec.dnsNames.0 - .spec.dnsNames.0
- .spec.dnsNames.1 - .spec.dnsNames.1
@ -74,6 +85,7 @@ replacements:
kind: Certificate kind: Certificate
group: cert-manager.io group: cert-manager.io
version: v1 version: v1
name: serving-cert
fieldPaths: fieldPaths:
- .spec.dnsNames.0 - .spec.dnsNames.0
- .spec.dnsNames.1 - .spec.dnsNames.1

1
config/dev/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
studio-credentials-secret.yaml

View file

@ -0,0 +1,55 @@
---
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
kind: APIGateway
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: gateway-sample
namespace: supabase-demo
spec:
envoy:
disableIPv6: true
workloadSpec:
replicas: 2
apiEndpoint:
jwks:
name: core-sample-jwt
key: jwks.json
dashboardEndpoint:
tls:
cert:
secretName: dashboard-tls-cert
auth:
oauth2:
openIdIssuer: "https://login.microsoftonline.com/f4e80111-1571-477a-b56d-c5fe517676b7/"
clientId: 3528016b-f6e3-49be-8fb3-f9a9a2ab6c3f
scopes:
- openid
- profile
- email
clientSecretRef:
name: studio-sample-oauth2
key: clientSecret
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
labels:
app.kubernetes.io/name: certificate
app.kubernetes.io/instance: dashboard-tls
app.kubernetes.io/component: certificate
app.kubernetes.io/created-by: supabase-operator
app.kubernetes.io/part-of: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: dashboard-tls
namespace: supabase-demo
spec:
dnsNames:
- gateway-sample-envoy.supabase-demo.svc
- gateway-sample-envoy.supabase-demo.svc.cluster.local
- localhost:3000
issuerRef:
kind: ClusterIssuer
name: cluster-pki
secretName: dashboard-tls-cert

36
config/dev/ca.yaml Normal file
View file

@ -0,0 +1,36 @@
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: cluster-pki-bootstrapper
namespace: cert-manager
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: cluster-pki-ca
namespace: cert-manager
spec:
commonName: cluster-pki
isCA: true
issuerRef:
kind: Issuer
name: cluster-pki-bootstrapper
secretName: cluster-pki-ca-cert
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: cluster-pki
namespace: cert-manager
spec:
ca:
secretName: cluster-pki-ca-cert

View file

@ -3,6 +3,7 @@ apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: pgsodium-config name: pgsodium-config
namespace: supabase-demo
data: data:
pgsodium_getkey.sh: | pgsodium_getkey.sh: |
#!/bin/bash #!/bin/bash
@ -18,6 +19,7 @@ apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
name: pgsodium-key name: pgsodium-key
namespace: supabase-demo
data: data:
# Generate a 32-byte key # Generate a 32-byte key
# head -c 32 /dev/urandom | od -A n -t x1 | tr -d ' \n' | base64 # head -c 32 /dev/urandom | od -A n -t x1 | tr -d ' \n' | base64
@ -27,6 +29,7 @@ apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
name: supabase-admin-credentials name: supabase-admin-credentials
namespace: supabase-demo
labels: labels:
cnpg.io/reload: "true" cnpg.io/reload: "true"
type: kubernetes.io/basic-auth type: kubernetes.io/basic-auth
@ -38,16 +41,18 @@ apiVersion: postgresql.cnpg.io/v1
kind: Cluster kind: Cluster
metadata: metadata:
name: cluster-example name: cluster-example
namespace: supabase-demo
spec: spec:
instances: 1 instances: 1
imageName: ghcr.io/supabase/postgres:15.8.1.021 imageName: registry.icb4dc0.de/supabase-operator/postgres:17.4
postgresUID: 105 imagePullPolicy: Always
postgresGID: 106 postgresUID: 26
postgresGID: 102
bootstrap: bootstrap:
initdb: initdb:
database: app database: app
owner: setup owner: supabase_admin
postInitSQL: postInitSQL:
- drop publication if exists supabase_realtime; - drop publication if exists supabase_realtime;

44
config/dev/core.yaml Normal file
View file

@ -0,0 +1,44 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supabase-demo-credentials
namespace: supabase-demo
stringData:
url: postgresql://supabase_admin:1n1t-R00t!@cluster-example-rw.supabase-demo:5432/app
---
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
kind: Core
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: core-sample
namespace: supabase-demo
spec:
# public URL of the Supabase instance (API)
# normally the Ingress/HTTPRoute endpoint
externalUrl: http://localhost:8000/
# public URL of the frontend
# could be the same as the externalUrl if you're using one Ingress/HTTPRoute for both
# or a different one if you prefer to separate API and frontend URLs
# will be used by Supabase Auth to redirect users after login
siteUrl: http://localhost:3000/
database:
# this field is write-only, it can be used to configure the DSN without having to create the secret manually
# dsn: postgresql://supabase_admin:1n1t-R00t!@cluster-example-rw.supabase-demo:5432/app
dsnSecretRef:
name: supabase-demo-credentials
key: url
auth:
disableSignup: false
enableEmailAutoconfirm: true
providers: {}
postgrest:
maxRows: 1000
jwt:
expiry: 3600
# name of the secret containing the JWT secret
# will be created if not found, make sure to refernce this secret in the APIGateway, Dashboard and Storage
secretName: core-sample-jwt

19
config/dev/dashboard.yaml Normal file
View file

@ -0,0 +1,19 @@
---
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
kind: Dashboard
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: dashboard-sample
namespace: supabase-demo
spec:
db:
host: cluster-example-rw.supabase-demo.svc
dbName: app
dbCredentialsRef:
secretName: core-sample-db-creds-supabase-admin
studio:
externalUrl: http://localhost:8000
jwt:
secretName: core-sample-jwt

View file

@ -2,10 +2,19 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
- https://github.com/cert-manager/cert-manager/releases/download/v1.16.3/cert-manager.yaml - https://github.com/cert-manager/cert-manager/releases/download/v1.17.1/cert-manager.yaml
- https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v1.25.0/cnpg-1.25.0.yaml - https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v1.25.0/cnpg-1.25.0.yaml
- resources/minio.yaml - ca.yaml
- namespace.yaml
- cnpg-cluster.yaml
- minio.yaml
- ../default - ../default
- studio-plaintext-users.yaml
- studio-credentials-secret.yaml
- core.yaml
- apigateway.yaml
- dashboard.yaml
- storage.yaml
patches: patches:
- path: manager_dev_settings.yaml - path: manager_dev_settings.yaml
@ -16,3 +25,6 @@ patches:
target: target:
kind: Deployment kind: Deployment
labelSelector: app.kubernetes.io/name=control-plane labelSelector: app.kubernetes.io/name=control-plane
configurations:
- kustomizeconfig/cnpg-cluster.yaml

View file

@ -0,0 +1,4 @@
images:
- kind: Cluster
group: postgresql.cnpg.io
path: spec/imageName

View file

@ -1,3 +0,0 @@
- op: replace
path: /spec/replicas
value: 1

View file

@ -1,4 +1,4 @@
# Deploys a new Namespace for the MinIO Pod ---
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: supabase-demo

43
config/dev/storage.yaml Normal file
View file

@ -0,0 +1,43 @@
---
apiVersion: v1
kind: Secret
metadata:
name: storage-s3-credentials
namespace: supabase-demo
stringData:
accessKeyId: FPxTAFL7NaubjPgIGBo3
secretAccessKey: 7F437pPe84QcoocD3MWdAIVBU3oXonhVHxK645tm
---
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
kind: Storage
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: storage-sample
namespace: supabase-demo
spec:
api:
s3Backend:
endpoint: http://minio.minio-dev.svc:9000
region: us-east-1
forcePathStyle: true
bucket: test
credentialsSecretRef:
secretName: storage-s3-credentials
s3: {}
db:
host: cluster-example-rw.supabase-demo.svc
dbName: app
dbCredentialsRef:
# will be created by Core resource operator if not present
# just make sure the secret name is either based on the name of the core resource or explicitly set
# format <core-resource-name>-db-creds-supabase-storage-admin
secretName: core-sample-db-creds-supabase-storage-admin
enableImageTransformation: true
jwtAuth:
# will be created by Core resource operator if not present
# just make sure the secret name is either based on the name of the core resource or explicitly set
secretName: core-sample-jwt
imageProxy:
enable: true

View file

@ -0,0 +1,8 @@
---
apiVersion: v1
kind: Secret
metadata:
name: studio-sample-oauth2
namespace: supabase-demo
stringData:
clientSecret: "G9r8Q~o4LJRlTQwPpdCBaZLsWdhUxM_02Y_XBcEr"

View file

@ -0,0 +1,8 @@
---
apiVersion: v1
kind: Secret
metadata:
name: studio-sample-basic-auth
namespace: supabase-demo
stringData:
ted: not_admin

View file

@ -20,26 +20,6 @@ spec:
labels: labels:
control-plane: controller-manager control-plane: controller-manager
spec: 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: securityContext:
runAsNonRoot: true runAsNonRoot: true
seccompProfile: seccompProfile:
@ -49,13 +29,17 @@ spec:
- manager - manager
- --leader-elect - --leader-elect
- --health-probe-bind-address=:8081 - --health-probe-bind-address=:8081
image: supabase-operator:latest image: controller:latest
name: manager name: manager
env: env:
- name: CONTROLLER_NAMESPACE - name: CONTROLLER_NAMESPACE
valueFrom: valueFrom:
fieldRef: fieldRef:
fieldPath: metadata.namespace fieldPath: metadata.namespace
- name: TLS_CA_CERT
value: /etc/supabase/operator/certs/tls.crt
- name: TLS_CA_KEY
value: /etc/supabase/operator/certs/tls.key
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
@ -73,8 +57,6 @@ spec:
port: 8081 port: 8081
initialDelaySeconds: 5 initialDelaySeconds: 5
periodSeconds: 10 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: resources:
limits: limits:
cpu: 150m cpu: 150m
@ -82,5 +64,12 @@ spec:
requests: requests:
cpu: 10m cpu: 10m
memory: 64Mi memory: 64Mi
volumeMounts:
- name: tls-certs
mountPath: /etc/supabase/operator/certs
volumes:
- name: tls-certs
secret:
secretName: control-plane-ca-cert-tls
serviceAccountName: controller-manager serviceAccountName: controller-manager
terminationGracePeriodSeconds: 10 terminationGracePeriodSeconds: 10

View file

@ -4,6 +4,17 @@ kind: ClusterRole
metadata: metadata:
name: control-plane-role name: control-plane-role
rules: rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- update
- get
- list
- watch
- apiGroups: - apiGroups:
- supabase.k8s.icb4dc0.de - supabase.k8s.icb4dc0.de
resources: resources:
@ -12,6 +23,8 @@ rules:
- get - get
- list - list
- watch - watch
- update
- apiGroups: - apiGroups:
- supabase.k8s.icb4dc0.de - supabase.k8s.icb4dc0.de
resources: resources:

View file

@ -2,9 +2,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
images: images:
- name: supabase-operator - name: controller
newName: code.icb4dc0.de/prskr/supabase-operator newName: registry.icb4dc0.de/supabase-operator/controller
newTag: 0.1.0-SNAPSHOT-e6c7d68 newTag: 0.1.0-SNAPSHOT-e6c7d68
resources: resources:
- ../../default - ../../default

View file

@ -4,7 +4,6 @@ namespace: supabase-demo
resources: resources:
- namespace.yaml - namespace.yaml
- cnpg-cluster.yaml
- supabase_v1alpha1_core.yaml - supabase_v1alpha1_core.yaml
- supabase_v1alpha1_apigateway.yaml - supabase_v1alpha1_apigateway.yaml
- supabase_v1alpha1_dashboard.yaml - supabase_v1alpha1_dashboard.yaml

View file

@ -6,8 +6,9 @@ metadata:
app.kubernetes.io/managed-by: kustomize app.kubernetes.io/managed-by: kustomize
name: gateway-sample name: gateway-sample
spec: spec:
jwks: apiEndpoint:
# will be created by Core resource operator if not present jwks:
# just make sure the secret name is either based on the name of the core resource or explicitly set # will be created by Core resource operator if not present
name: core-sample-jwt # just make sure the secret name is either based on the name of the core resource or explicitly set
key: jwks.json name: core-sample-jwt
key: jwks.json

View file

@ -0,0 +1,30 @@
---
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
kind: APIGateway
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: gateway-sample
spec:
apiEndpoint:
jwks:
# will be created by Core resource operator if not present
# just make sure the secret name is either based on the name of the core resource or explicitly set
name: core-sample-jwt
key: jwks.json
dashboardEndpoint:
auth:
basic:
usersInline:
# admin:admin
- admin:{SHA}0DPiKuNIrrVmD8IUCuw1hQxNqZc=
plaintextUsersSecretRef: studio-sample-basic-auth
---
apiVersion: v1
kind: Secret
metadata:
name: studio-sample-basic-auth
namespace: supabase-demo
stringData:
ted: not_admin

View file

@ -0,0 +1,36 @@
---
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
kind: APIGateway
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: gateway-sample
spec:
apiEndpoint:
jwks:
# will be created by Core resource operator if not present
# just make sure the secret name is either based on the name of the core resource or explicitly set
name: core-sample-jwt
key: jwks.json
dashboardEndpoint:
auth:
oauth2:
openIdIssuer: "https://idp.your-domain.com/"
clientId: "<your-client-id>"
# if not set, 'user' will be used
scopes:
- openid
- profile
- email
clientSecretRef:
name: studio-sample-oauth2
key: clientSecret
---
apiVersion: v1
kind: Secret
metadata:
name: studio-sample-oauth2
namespace: supabase-demo
stringData:
clientSecret: "<your-client-secret>"

View file

@ -1,17 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: supabase-demo-credentials
stringData:
url: postgresql://supabase_admin:1n1t-R00t!@cluster-example-rw.supabase-demo:5432/app
--- ---
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1 apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
kind: Core kind: Core
metadata: metadata:
name: core-sample
labels: labels:
app.kubernetes.io/name: supabase-operator app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize app.kubernetes.io/managed-by: kustomize
name: core-sample
spec: spec:
# public URL of the Supabase instance (API) # public URL of the Supabase instance (API)
# normally the Ingress/HTTPRoute endpoint # normally the Ingress/HTTPRoute endpoint
@ -24,10 +18,10 @@ spec:
siteUrl: http://localhost:3000/ siteUrl: http://localhost:3000/
database: database:
dsnSecretRef: dsnSecretRef:
# name of the secret containing the database DSN
name: supabase-demo-credentials name: supabase-demo-credentials
key: url key: url
auth: auth:
disableSignup: false
enableEmailAutoconfirm: true enableEmailAutoconfirm: true
providers: {} providers: {}
postgrest: postgrest:
@ -35,5 +29,8 @@ spec:
jwt: jwt:
expiry: 3600 expiry: 3600
# name of the secret containing the JWT secret # name of the secret containing the JWT secret
# will be created if not found, make sure to refernce this secret in the APIGateway, Dashboard and Storage # will be created if not found, make sure to refernce this secret in:
# - APIGateway
# - Dashboard-
# - Storage
secretName: core-sample-jwt secretName: core-sample-jwt

View file

@ -1,12 +1,4 @@
--- ---
apiVersion: v1
kind: Secret
metadata:
name: storage-s3-credentials
stringData:
accessKeyId: FPxTAFL7NaubjPgIGBo3
secretAccessKey: 7F437pPe84QcoocD3MWdAIVBU3oXonhVHxK645tm
---
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1 apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
kind: Storage kind: Storage
metadata: metadata:
@ -23,7 +15,7 @@ spec:
bucket: test bucket: test
credentialsSecretRef: credentialsSecretRef:
secretName: storage-s3-credentials secretName: storage-s3-credentials
s3Protocol: {} s3: {}
db: db:
host: cluster-example-rw.supabase-demo.svc host: cluster-example-rw.supabase-demo.svc
dbName: app dbName: app

View file

@ -1,9 +1,13 @@
apiVersion: ctlptl.dev/v1alpha1 apiVersion: ctlptl.dev/v1alpha1
kind: Registry kind: Registry
name: ctlptl-registry name: supabase-operator-registry
port: 5005 port: 5005
--- ---
apiVersion: ctlptl.dev/v1alpha1 apiVersion: ctlptl.dev/v1alpha1
kind: Cluster kind: Cluster
product: kind product: kind
registry: ctlptl-registry registry: supabase-operator-registry
kindV1Alpha4Cluster:
name: supabase-operator-debug
networking:
ipFamily: dual

View file

@ -71,13 +71,31 @@ _Appears in:_
| Field | Description | Default | Validation | | Field | Description | Default | Validation |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `envoy` _[EnvoySpec](#envoyspec)_ | Envoy - configure the envoy instance and most importantly the control-plane | | | | `envoy` _[EnvoySpec](#envoyspec)_ | Envoy - configure the envoy instance and most importantly the control-plane | | |
| `jwks` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretkeyselector-v1-core)_ | JWKSSelector - selector where the JWKS can be retrieved from to enable the API gateway to validate JWTs | | | | `apiEndpoint` _[ApiEndpointSpec](#apiendpointspec)_ | ApiEndpoint - Configure the endpoint for all API routes<br />this includes the JWT configuration | | |
| `dashboardEndpoint` _[DashboardEndpointSpec](#dashboardendpointspec)_ | DashboardEndpoint - Configure the endpoint for the Supabase dashboard (studio)<br />this includes optional authentication (basic or Oauth2) for the dashboard | | |
| `serviceSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#labelselector-v1-meta)_ | ServiceSelector - selector to match all Supabase services (or in fact EndpointSlices) that should be considered for this APIGateway | \{ matchExpressions:[map[key:app.kubernetes.io/part-of operator:In values:[supabase]] map[key:supabase.k8s.icb4dc0.de/api-gateway-target operator:Exists]] \} | | | `serviceSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#labelselector-v1-meta)_ | ServiceSelector - selector to match all Supabase services (or in fact EndpointSlices) that should be considered for this APIGateway | \{ matchExpressions:[map[key:app.kubernetes.io/part-of operator:In values:[supabase]] map[key:supabase.k8s.icb4dc0.de/api-gateway-target operator:Exists]] \} | |
| `componentTypeLabel` _string_ | ComponentTypeLabel - Label to identify which Supabase component a Service represents (e.g. auth, postgrest, ...) | app.kubernetes.io/name | | | `componentTypeLabel` _string_ | ComponentTypeLabel - Label to identify which Supabase component a Service represents (e.g. auth, postgrest, ...) | app.kubernetes.io/name | |
#### ApiEndpointSpec
_Appears in:_
- [APIGatewaySpec](#apigatewayspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `jwks` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretkeyselector-v1-core)_ | JWKSSelector - selector where the JWKS can be retrieved from to enable the API gateway to validate JWTs | | |
| `tls` _[EndpointTlsSpec](#endpointtlsspec)_ | TLS - enable and configure TLS for the API endpoint | | |
#### AuthProviderMeta #### AuthProviderMeta
@ -133,7 +151,7 @@ _Appears in:_
| `disableSignup` _boolean_ | | | | | `disableSignup` _boolean_ | | | |
| `anonymousUsersEnabled` _boolean_ | | | | | `anonymousUsersEnabled` _boolean_ | | | |
| `providers` _[AuthProviders](#authproviders)_ | | | | | `providers` _[AuthProviders](#authproviders)_ | | | |
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | | | | | `workloadTemplate` _[WorkloadSpec](#workloadspec)_ | | | |
| `emailSignupDisabled` _boolean_ | | | | | `emailSignupDisabled` _boolean_ | | | |
@ -167,7 +185,7 @@ _Appears in:_
_Appears in:_ _Appears in:_
- [WorkloadTemplate](#workloadtemplate) - [WorkloadSpec](#workloadspec)
| Field | Description | Default | Validation | | Field | Description | Default | Validation |
| --- | --- | --- | --- | | --- | --- | --- | --- |
@ -300,6 +318,42 @@ _Appears in:_
| `spec` _[DashboardSpec](#dashboardspec)_ | | | | | `spec` _[DashboardSpec](#dashboardspec)_ | | | |
#### DashboardAuthSpec
_Appears in:_
- [DashboardEndpointSpec](#dashboardendpointspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `oauth2` _[DashboardOAuth2Spec](#dashboardoauth2spec)_ | OAuth2 - configure oauth2 authentication for the dashhboard listener<br />if configured, will be preferred over Basic authentication configuration<br />effectively disabling basic auth | | |
| `basic` _[DashboardBasicAuthSpec](#dashboardbasicauthspec)_ | Basic - HTTP basic auth configuration, this should only be used in exceptions<br />e.g. during evaluations or for local development<br />only used if no other authentication is configured | | |
#### DashboardBasicAuthSpec
_Appears in:_
- [DashboardAuthSpec](#dashboardauthspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `usersInline` _string array_ | UsersInline - [htpasswd format](https://httpd.apache.org/docs/2.4/programs/htpasswd.html) | | items:Pattern: ^[\w_.]+:\\{SHA\\}[A-z0-9]+=*$ <br /> |
| `plaintextUsersSecretRef` _string_ | PlaintextUsersSecretRef - name of a secret that contains plaintext credentials in key-value form<br />if not empty, credentials will be merged with inline users | | |
#### DashboardDbSpec #### DashboardDbSpec
@ -319,6 +373,23 @@ _Appears in:_
| `dbCredentialsRef` _[DbCredentialsReference](#dbcredentialsreference)_ | DBCredentialsRef - reference to a Secret key where the DB credentials can be retrieved from<br />Credentials need to be stored in basic auth form | | | | `dbCredentialsRef` _[DbCredentialsReference](#dbcredentialsreference)_ | DBCredentialsRef - reference to a Secret key where the DB credentials can be retrieved from<br />Credentials need to be stored in basic auth form | | |
#### DashboardEndpointSpec
_Appears in:_
- [APIGatewaySpec](#apigatewayspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `auth` _[DashboardAuthSpec](#dashboardauthspec)_ | Auth - configure authentication for the dashboard endpoint | | |
| `tls` _[EndpointTlsSpec](#endpointtlsspec)_ | TLS - enable and configure TLS for the Dashboard endpoint | | |
#### DashboardList #### DashboardList
@ -337,6 +408,28 @@ DashboardList contains a list of Dashboard.
| `items` _[Dashboard](#dashboard) array_ | | | | | `items` _[Dashboard](#dashboard) array_ | | | |
#### DashboardOAuth2Spec
_Appears in:_
- [DashboardAuthSpec](#dashboardauthspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `openIdIssuer` _string_ | OpenIDIssuer - if set the defaulter will fetch the discovery document and fill<br />TokenEndpoint and AuthorizationEndpoint based on the discovery document | | |
| `tokenEndpoint` _string_ | TokenEndpoint - endpoint where Envoy will retrieve the OAuth2 access and identity token from | | |
| `authorizationEndpoint` _string_ | AuthorizationEndpoint - endpoint where the user will be redirected to authenticate | | |
| `clientId` _string_ | ClientID - client ID to authenticate with the OAuth2 provider | | |
| `scopes` _string array_ | Scopes - scopes to request from the OAuth2 provider (e.g. "openid", "profile", ...) - optional | | |
| `resources` _string array_ | Resources - resources to request from the OAuth2 provider (e.g. "user", "email", ...) - optional | | |
| `clientSecretRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretkeyselector-v1-core)_ | ClientSecretRef - reference to the secret that contains the client secret | | |
#### DashboardSpec #### DashboardSpec
@ -425,7 +518,7 @@ _Appears in:_
| Field | Description | Default | Validation | | Field | Description | Default | Validation |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `appliedMigrations` _[MigrationStatus](#migrationstatus)_ | | | | | `migrationConditions` _[MigrationScriptCondition](#migrationscriptcondition) array_ | | | |
| `roles` _object (keys:string, values:integer array)_ | | | | | `roles` _object (keys:string, values:integer array)_ | | | |
@ -489,6 +582,69 @@ _Appears in:_
| `credentialsRef` _[SmtpCredentialsReference](#smtpcredentialsreference)_ | | | | | `credentialsRef` _[SmtpCredentialsReference](#smtpcredentialsreference)_ | | | |
#### EndpointTlsSpec
_Appears in:_
- [ApiEndpointSpec](#apiendpointspec)
- [DashboardEndpointSpec](#dashboardendpointspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `cert` _[TlsCertRef](#tlscertref)_ | | | |
#### EnvoyComponentLogLevel
_Appears in:_
- [EnvoyDebuggingOptions](#envoydebuggingoptions)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `component` _string_ | Component - the component to set the log level for<br />the component IDs can be found [here](https://github.com/envoyproxy/envoy/blob/main/source/common/common/logger.h#L36) | | |
| `level` _[EnvoyLogLevel](#envoyloglevel)_ | Level - the log level to set for the component | | Enum: [trace debug info warning error critical off] <br /> |
#### EnvoyDebuggingOptions
_Appears in:_
- [EnvoySpec](#envoyspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `componentLogLevels` _[EnvoyComponentLogLevel](#envoycomponentloglevel) array_ | | | |
#### EnvoyLogLevel
_Underlying type:_ _string_
_Appears in:_
- [EnvoyComponentLogLevel](#envoycomponentloglevel)
#### EnvoySpec #### EnvoySpec
@ -504,7 +660,9 @@ _Appears in:_
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `nodeName` _string_ | NodeName - identifies the Envoy cluster within the current namespace<br />if not set, the name of the APIGateway resource will be used<br />The primary use case is to make the assignment of multiple supabase instances in a single namespace explicit. | | | | `nodeName` _string_ | NodeName - identifies the Envoy cluster within the current namespace<br />if not set, the name of the APIGateway resource will be used<br />The primary use case is to make the assignment of multiple supabase instances in a single namespace explicit. | | |
| `controlPlane` _[ControlPlaneSpec](#controlplanespec)_ | ControlPlane - configure the control plane where Envoy will retrieve its configuration from | | | | `controlPlane` _[ControlPlaneSpec](#controlplanespec)_ | ControlPlane - configure the control plane where Envoy will retrieve its configuration from | | |
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the Envoy deployment | | | | `workloadSpec` _[WorkloadSpec](#workloadspec)_ | WorkloadTemplate - customize the Envoy deployment | | |
| `disableIPv6` _boolean_ | DisableIPv6 - disable IPv6 for the Envoy instance<br />this will force Envoy to use IPv4 for upstream hosts (mostly for the OAuth2 token endpoint) | | |
| `debugging` _[EnvoyDebuggingOptions](#envoydebuggingoptions)_ | | | |
#### EnvoyStatus #### EnvoyStatus
@ -520,7 +678,6 @@ _Appears in:_
| Field | Description | Default | Validation | | Field | Description | Default | Validation |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `configVersion` _string_ | | | |
| `resourceHash` _integer array_ | | | | | `resourceHash` _integer array_ | | | |
@ -574,7 +731,7 @@ _Appears in:_
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `enable` _boolean_ | Enable - whether to deploy the image proxy or not | | | | `enable` _boolean_ | Enable - whether to deploy the image proxy or not | | |
| `enableWebPDetection` _boolean_ | | | | | `enableWebPDetection` _boolean_ | | | |
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the image proxy workload | | | | `workloadSpec` _[WorkloadSpec](#workloadspec)_ | WorkloadTemplate - customize the image proxy workload | | |
#### ImageSpec #### ImageSpec
@ -616,9 +773,11 @@ _Appears in:_
| `serviceKey` _string_ | ServiceKey - key in secret where to read the service JWT from | service_key | | | `serviceKey` _string_ | ServiceKey - key in secret where to read the service JWT from | service_key | |
#### MigrationStatus
_Underlying type:_ _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_
#### MigrationScriptCondition
@ -627,6 +786,14 @@ _Underlying type:_ _[Time](https://kubernetes.io/docs/reference/generated/kubern
_Appears in:_ _Appears in:_
- [DatabaseStatus](#databasestatus) - [DatabaseStatus](#databasestatus)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `name` _string_ | Name - file name of the migration script | | |
| `hash` _integer array_ | Hash - SHA256 hash of the script when it was last successfully applied | | |
| `lastProbeTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_ | LastProbeTime - last time the operator tried to execute the migration script | | |
| `lastTransitionTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_ | LastTransitionTime - last time the condition transitioned from one status to another | | |
| `reason` _string_ | Reason - one-word, CamcelCase reason for the condition's last transition | | |
| `message` _string_ | Message - human-readable message indicating details about the last transition | | |
#### OAuthProvider #### OAuthProvider
@ -661,7 +828,7 @@ _Appears in:_
| Field | Description | Default | Validation | | Field | Description | Default | Validation |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the pg-meta deployment | | | | `workloadSpec` _[WorkloadSpec](#workloadspec)_ | WorkloadTemplate - customize the pg-meta deployment | | |
#### PhoneAuthProvider #### PhoneAuthProvider
@ -697,7 +864,7 @@ _Appears in:_
| `extraSearchPath` _string array_ | ExtraSearchPath - Extra schemas to add to the search_path of every request.<br />These schemas tables, views and functions don’t get API endpoints, they can only be referred from the database objects inside your db-schemas. | [public extensions] | | | `extraSearchPath` _string array_ | ExtraSearchPath - Extra schemas to add to the search_path of every request.<br />These schemas tables, views and functions don’t get API endpoints, they can only be referred from the database objects inside your db-schemas. | [public extensions] | |
| `anonRole` _string_ | AnonRole - name of the anon role | anon | | | `anonRole` _string_ | AnonRole - name of the anon role | anon | |
| `maxRows` _integer_ | MaxRows - maximum number of rows PostgREST will load at a time | 1000 | | | `maxRows` _integer_ | MaxRows - maximum number of rows PostgREST will load at a time | 1000 | |
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the PostgREST workload | | | | `workloadSpec` _[WorkloadSpec](#workloadspec)_ | WorkloadSpec - customize the PostgREST workload | | |
#### S3BackendSpec #### S3BackendSpec
@ -832,7 +999,7 @@ _Appears in:_
| `db` _[StorageApiDbSpec](#storageapidbspec)_ | DBSpec - Configure access to the Postgres database<br />In most cases this will reference the supabase-storage-admin credentials secret provided by the Core resource | | | | `db` _[StorageApiDbSpec](#storageapidbspec)_ | DBSpec - Configure access to the Postgres database<br />In most cases this will reference the supabase-storage-admin credentials secret provided by the Core resource | | |
| `s3` _[S3ProtocolSpec](#s3protocolspec)_ | S3Protocol - Configure S3 access to the Storage API allowing clients to use any S3 client | | | | `s3` _[S3ProtocolSpec](#s3protocolspec)_ | S3Protocol - Configure S3 access to the Storage API allowing clients to use any S3 client | | |
| `uploadTemp` _[UploadTempSpec](#uploadtempspec)_ | UploadTemp - configure the emptyDir for storing intermediate files during uploads | | | | `uploadTemp` _[UploadTempSpec](#uploadtempspec)_ | UploadTemp - configure the emptyDir for storing intermediate files during uploads | | |
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the Storage API workload | | | | `workloadSpec` _[WorkloadSpec](#workloadspec)_ | WorkloadTemplate - customize the Storage API workload | | |
#### StorageList #### StorageList
@ -886,11 +1053,30 @@ _Appears in:_
| Field | Description | Default | Validation | | Field | Description | Default | Validation |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `jwt` _[JwtSpec](#jwtspec)_ | | | | | `jwt` _[JwtSpec](#jwtspec)_ | | | |
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the studio deployment | | | | `workloadSpec` _[WorkloadSpec](#workloadspec)_ | WorkloadTemplate - customize the studio deployment | | |
| `gatewayServiceSelector` _object (keys:string, values:string)_ | GatewayServiceSelector - selector to find the service for the API gateway<br />Required to configure the API URL in the studio deployment<br />If you don't run multiple APIGateway instances in the same namespaces, the default will be fine | \{ app.kubernetes.io/component:api-gateway app.kubernetes.io/name:envoy \} | | | `gatewayServiceSelector` _object (keys:string, values:string)_ | GatewayServiceSelector - selector to find the service for the API gateway<br />Required to configure the API URL in the studio deployment<br />If you don't run multiple APIGateway instances in the same namespaces, the default will be fine | \{ app.kubernetes.io/component:api-gateway app.kubernetes.io/name:envoy \} | |
| `externalUrl` _string_ | APIExternalURL is referring to the URL where Supabase API will be available<br />Typically this is the ingress of the API gateway | | | | `externalUrl` _string_ | APIExternalURL is referring to the URL where Supabase API will be available<br />Typically this is the ingress of the API gateway | | |
#### TlsCertRef
_Appears in:_
- [EndpointTlsSpec](#endpointtlsspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `secretName` _string_ | | | |
| `serverCertKey` _string_ | ServerCertKey - key in the secret that contains the server certificate | tls.crt | |
| `serverKeyKey` _string_ | ServerKeyKey - key in the secret that contains the server private key | tls.key | |
| `caCertKey` _string_ | CaCertKey - key in the secret that contains the CA certificate | ca.crt | |
#### UploadTempSpec #### UploadTempSpec
@ -908,7 +1094,7 @@ _Appears in:_
| `sizeLimit` _[Quantity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#quantity-resource-api)_ | | | | | `sizeLimit` _[Quantity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#quantity-resource-api)_ | | | |
#### WorkloadTemplate #### WorkloadSpec
@ -930,7 +1116,7 @@ _Appears in:_
| `replicas` _integer_ | | | | | `replicas` _integer_ | | | |
| `securityContext` _[PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#podsecuritycontext-v1-core)_ | | | | | `securityContext` _[PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#podsecuritycontext-v1-core)_ | | | |
| `additionalLabels` _object (keys:string, values:string)_ | | | | | `additionalLabels` _object (keys:string, values:string)_ | | | |
| `workload` _[ContainerTemplate](#containertemplate)_ | Workload - customize the container template of the workload | | | | `container` _[ContainerTemplate](#containertemplate)_ | ContainerSpec - customize the container template of the workload | | |
| `additionalVolumes` _[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volume-v1-core) array_ | | | | | `additionalVolumes` _[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volume-v1-core) array_ | | | |

1
docs/auth/email.md Normal file
View file

@ -0,0 +1 @@
# Email

1
docs/auth/overview.md Normal file
View file

@ -0,0 +1 @@
# Overview

View file

@ -0,0 +1 @@
# Providers

View file

@ -0,0 +1 @@
# APIGateway

View file

@ -0,0 +1,14 @@
# Core
The `Core` resource configures the essential Supabase services:
- PostgREST
- Auth
and it manages the initial DB migrations that are not done by the services themselves and are part of the [supabase/postgres](https://github.com/supabase/postgres) repository.
To deploy a basic `Core` instance, you can use the following snippet as a 'template':
```yaml title="Basic 'Core' example" linenums="1"
--8<-- "config/samples/supabase_v1alpha1_core.yaml"
```

19
docs/development/tools.md Normal file
View file

@ -0,0 +1,19 @@
# Tools
## Essentials
- Current Go version (see `go.mod`)
- Any Docker daemon[^1]
- kubectl (v1.30.0+.)
- Access to a Kubernetes v1.30.+ cluster[^2]
[^1]: Docker Desktop, Orbstack, Podman, ...
[^2]: kind, Orbstack, Docker Desktop, Rancher Desktop, k3s, microk8s, ...
## Recommended
- [husky](https://github.com/go-courier/husky)
- [tilt](https://tilt.dev/)
- [ctlptl](https://github.com/tilt-dev/ctlptl)
- [goreleaser](https://goreleaser.com/)
- [ko](https://ko.build/)

View file

@ -1,5 +1,11 @@
# Getting Started # Getting Started
## Dependencies
The operator has the following dependencies:
- [cert-manager](https://cert-manager.io/) (to issue webhook certificates)
## Deploying the operator ## Deploying the operator
The easiest way to deploy the operator is to fetch the manifest from the [releases](https://code.icb4dc0.de/prskr/supabase-operator/releases) and apply it like this: The easiest way to deploy the operator is to fetch the manifest from the [releases](https://code.icb4dc0.de/prskr/supabase-operator/releases) and apply it like this:
@ -12,3 +18,12 @@ The manifest is rendered as part of the release workflow and based on [kustomize
If you want to customize the deployment, you can start from the [release/default](https://code.icb4dc0.de/prskr/supabase-operator/src/branch/main/config/release/default) layer and build your own manifest. If you want to customize the deployment, you can start from the [release/default](https://code.icb4dc0.de/prskr/supabase-operator/src/branch/main/config/release/default) layer and build your own manifest.
## Deploying a basic Supabase instance ## Deploying a basic Supabase instance
As described in the [overview](./components/overview.md) the custom resources are 'grouping' the Supabase services into 'modules'.
A common basic instance requires:
- a [`Core`](./components/core.md) instance (PostgREST, Auth & DB migrations)
- an [`APIGateway`](./components/apigateway.md) instance (gateway to handle JWT auth and routing)
it is perfectly possible to deploy for instance only an `APIGateway` and a `Storage` instance as well if you don't need the API or you can also manage your own API gateway if you prefer it that way.
The operator setup tries to be as 'unopinionated' as possible.

View file

@ -9,7 +9,7 @@ This project is not affiliated with the Supabase project or company in any way.
This is currently a work-in-progress experiment to replace existing Helm charts for Supabase as they tend to be hard to deploy and to manage and the default Supabase stack - although working great as a single instance or in their SaaS instances - isn't a perfect fit for Kubernetes. This is currently a work-in-progress experiment to replace existing Helm charts for Supabase as they tend to be hard to deploy and to manage and the default Supabase stack - although working great as a single instance or in their SaaS instances - isn't a perfect fit for Kubernetes.
This operator replaces tedious Helm values files with a small set of custom resources that allow an user to quickly deploy a Supabase instance without having to know much (if anything) of the Supabase internals. This operator replaces tedious Helm values files with a small set of custom resources that allow an user to quickly deploy a Supabase instance without having to know much (if anything) of the Supabase internals.
## Targets ## Goals
- Make it as easy as possible to deploy Supabase on a Kubernetes cluster - Make it as easy as possible to deploy Supabase on a Kubernetes cluster
- Manage updates of components - Manage updates of components
@ -23,7 +23,7 @@ This operator replaces tedious Helm values files with a small set of custom reso
- ConfigMaps - ConfigMaps
- *soon*: NetworkPolicies - *soon*: NetworkPolicies
## Non-Targets ## Non-Goals
- Manage **all** Kubernetes aspects, it does **not** create: - Manage **all** Kubernetes aspects, it does **not** create:
- PodDisruptionBudgets - PodDisruptionBudgets

301
go.mod
View file

@ -1,119 +1,326 @@
module code.icb4dc0.de/prskr/supabase-operator module code.icb4dc0.de/prskr/supabase-operator
go 1.23.5 go 1.24
toolchain go1.24.0
require ( require (
github.com/alecthomas/kong v1.6.0 github.com/alecthomas/kong v1.7.0
github.com/envoyproxy/go-control-plane v0.13.1 github.com/envoyproxy/go-control-plane v0.13.4
github.com/jackc/pgx/v5 v5.7.1 github.com/envoyproxy/go-control-plane/envoy v1.32.4
github.com/go-logr/logr v1.4.2
github.com/jackc/pgx/v5 v5.7.2
github.com/lestrrat-go/jwx/v2 v2.1.3 github.com/lestrrat-go/jwx/v2 v2.1.3
github.com/magefile/mage v1.15.0 github.com/magefile/mage v1.15.0
github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/ginkgo/v2 v2.22.2
github.com/onsi/gomega v1.35.1 github.com/onsi/gomega v1.36.2
github.com/stretchr/testify v1.10.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c
google.golang.org/grpc v1.65.0 google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.3 google.golang.org/protobuf v1.36.6
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.32.1 k8s.io/api v0.32.1
k8s.io/apimachinery v0.32.1 k8s.io/apimachinery v0.32.1
k8s.io/client-go v0.32.1 k8s.io/client-go v0.32.1
sigs.k8s.io/controller-runtime v0.20.0 sigs.k8s.io/controller-runtime v0.20.1
)
tool (
github.com/elastic/crd-ref-docs
github.com/golangci/golangci-lint/v2/cmd/golangci-lint
gotest.tools/gotestsum
mvdan.cc/gofumpt
sigs.k8s.io/controller-runtime/tools/setup-envtest
sigs.k8s.io/controller-tools/cmd/controller-gen
sigs.k8s.io/kustomize/kustomize/v5
) )
require ( require (
cel.dev/expr v0.18.0 // indirect 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect cel.dev/expr v0.19.2 // indirect
github.com/4meepo/tagalign v1.4.2 // indirect
github.com/Abirdcfly/dupword v0.1.3 // indirect
github.com/Antonboom/errname v1.1.0 // indirect
github.com/Antonboom/nilnil v1.1.0 // indirect
github.com/Antonboom/testifylint v1.6.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/Crocmagnon/fatcontext v0.7.1 // indirect
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
github.com/alexkohler/nakedret/v2 v2.0.5 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/alingse/nilnesserr v0.1.2 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/ashanbrown/forbidigo v1.6.0 // indirect
github.com/ashanbrown/makezero v1.2.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bitfield/gotestdox v0.2.2 // indirect
github.com/bkielbasa/cyclop v1.2.3 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v4 v4.6.0 // indirect
github.com/breml/bidichk v0.3.3 // indirect
github.com/breml/errchkjson v0.4.1 // indirect
github.com/butuzov/ireturn v0.3.1 // indirect
github.com/butuzov/mirror v1.3.0 // indirect
github.com/catenacyber/perfsprint v0.9.1 // indirect
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // indirect github.com/charithe/durationcheck v0.0.10 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/chavacava/garif v0.1.0 // indirect
github.com/ckaznocha/intrange v0.3.1 // indirect
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
github.com/curioswitch/go-reassign v0.3.0 // indirect
github.com/daixiang0/gci v0.13.6 // indirect
github.com/dave/dst v0.27.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/dnephin/pflag v1.0.7 // indirect
github.com/elastic/crd-ref-docs v0.1.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/firefart/nonamedreturns v1.0.5 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v0.3.12 // indirect
github.com/go-critic/go-critic v0.13.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobuffalo/flect v1.0.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.11.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
github.com/golangci/go-printf-func-name v0.1.0 // indirect
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
github.com/golangci/golangci-lint/v2 v2.0.1 // indirect
github.com/golangci/golines v0.0.0-20250217232252-b35a6149b587 // indirect
github.com/golangci/misspell v0.6.0 // indirect
github.com/golangci/plugin-module-register v0.1.1 // indirect
github.com/golangci/revgrep v0.8.0 // indirect
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.22.0 // indirect github.com/google/cel-go v0.22.0 // indirect
github.com/google/gnostic-models v0.6.9 // indirect github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/pprof v0.0.0-20250202011525-fc3143867406 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/gordonklaus/ineffassign v0.1.0 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.5.0 // indirect
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0 // indirect
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jgautheron/goconst v1.7.1 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jjti/go-spancheck v0.6.4 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/julz/importas v0.2.0 // indirect
github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
github.com/kisielk/errcheck v1.9.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/compress v1.17.11 // indirect
github.com/kulti/thelper v0.6.3 // indirect
github.com/kunwardeep/paralleltest v1.0.12 // indirect
github.com/lasiar/canonicalheader v1.1.2 // indirect
github.com/ldez/exptostd v0.4.2 // indirect
github.com/ldez/gomoddirectives v0.6.1 // indirect
github.com/ldez/grignotin v0.9.0 // indirect
github.com/ldez/tagliatelle v0.7.1 // indirect
github.com/ldez/usetesting v0.4.2 // indirect
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect github.com/lestrrat-go/httprc v1.0.6 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/macabu/inamedparam v0.2.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect
github.com/maratori/testableexamples v1.0.0 // indirect
github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgechev/revive v1.7.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/moricho/tparallel v0.3.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nishanths/exhaustive v0.12.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.19.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/polyfloyd/go-errorlint v1.7.1 // indirect
github.com/prometheus/client_golang v1.21.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.16.0 // indirect
github.com/quasilyte/go-ruleguard v0.4.4 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/raeperd/recvcheck v0.2.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/ryancurrah/gomodguard v1.4.1 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sagikazarmark/locafero v0.8.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect
github.com/securego/gosec/v2 v2.22.2 // indirect
github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/asm v1.2.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect github.com/sergi/go-diff v1.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sonatard/noctx v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.20.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tdakkota/asciicheck v0.4.1 // indirect
github.com/tetafro/godot v1.5.0 // indirect
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
github.com/timonwong/loggercheck v0.10.1 // indirect
github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/ultraware/funlen v0.2.0 // indirect
github.com/ultraware/whitespace v0.2.0 // indirect
github.com/uudashr/gocognit v1.2.0 // indirect
github.com/uudashr/iface v1.3.1 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect github.com/yagipy/maintidx v1.0.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect go-simpler.org/musttag v0.13.0 // indirect
go-simpler.org/sloglint v0.9.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.32.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/oauth2 v0.25.0 // indirect golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/term v0.28.0 // indirect golang.org/x/sync v0.12.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/time v0.9.0 // indirect golang.org/x/term v0.30.0 // indirect
golang.org/x/tools v0.26.0 // indirect golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.10.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250127172529-29210b9bc287 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools/gotestsum v1.12.0 // indirect
honnef.co/go/tools v0.6.1 // indirect
k8s.io/apiextensions-apiserver v0.32.1 // indirect k8s.io/apiextensions-apiserver v0.32.1 // indirect
k8s.io/apiserver v0.32.1 // indirect k8s.io/apiserver v0.32.1 // indirect
k8s.io/component-base v0.32.1 // indirect k8s.io/component-base v0.32.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect mvdan.cc/gofumpt v0.7.0 // indirect
mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.1 // indirect
sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250205174817-cd644c0ad54f // indirect
sigs.k8s.io/controller-tools v0.17.2 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/kustomize/api v0.19.0 // indirect
sigs.k8s.io/kustomize/cmd/config v0.19.0 // indirect
sigs.k8s.io/kustomize/kustomize/v5 v5.6.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect
) )

764
go.sum
View file

@ -1,50 +1,173 @@
cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= 4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A=
cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= 4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4=
cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E=
github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw=
github.com/Antonboom/errname v1.1.0 h1:A+ucvdpMwlo/myWrkHEUEBWc/xuXdud23S8tmTb/oAE=
github.com/Antonboom/errname v1.1.0/go.mod h1:O1NMrzgUcVBGIfi3xlVuvX8Q/VP/73sseCaAppfjqZw=
github.com/Antonboom/nilnil v1.1.0 h1:jGxJxjgYS3VUUtOTNk8Z1icwT5ESpLH/426fjmQG+ng=
github.com/Antonboom/nilnil v1.1.0/go.mod h1:b7sAlogQjFa1wV8jUW3o4PMzDVFLbTux+xnQdvzdcIE=
github.com/Antonboom/testifylint v1.6.0 h1:6rdILVPt4+rqcvhid8w9wJNynKLUgqHNpFyM67UeXyc=
github.com/Antonboom/testifylint v1.6.0/go.mod h1:k+nEkathI2NFjKO6HvwmSrbzUcQ6FAnbZV+ZRrnXPLI=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Crocmagnon/fatcontext v0.7.1 h1:SC/VIbRRZQeQWj/TcQBS6JmrXcfA+BU4OGSVUt54PjM=
github.com/Crocmagnon/fatcontext v0.7.1/go.mod h1:1wMvv3NXEBJucFGfwOJBxSVWcoIO6emV215SMkW9MFU=
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM=
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k=
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.6.0 h1:mwOzbdMR7uv2vul9J0FU3GYxE7ls/iX1ieMg5WIM6gE= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU=
github.com/alecthomas/kong v1.6.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=
github.com/alecthomas/kong v1.7.0 h1:MnT8+5JxFDCvISeI6vgd/mFbAJwueJ/pqQNzZMsiqZE=
github.com/alecthomas/kong v1.7.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=
github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
github.com/alingse/nilnesserr v0.1.2 h1:Yf8Iwm3z2hUUrP4muWfW83DF4nE3r1xZ26fGWUKCZlo=
github.com/alingse/nilnesserr v0.1.2/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY=
github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU=
github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU=
github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CDsnE=
github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY=
github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 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/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
github.com/bombsimon/wsl/v4 v4.6.0 h1:ew2R/N42su553DKTYqt3HSxaQN+uHQPv4xZ2MBmwaW4=
github.com/bombsimon/wsl/v4 v4.6.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg=
github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE=
github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE=
github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg=
github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s=
github.com/butuzov/ireturn v0.3.1 h1:mFgbEI6m+9W8oP/oDdfA34dLisRFCj2G6o/yiI1yZrY=
github.com/butuzov/ireturn v0.3.1/go.mod h1:ZfRp+E7eJLC0NQmk1Nrm1LOrn/gQlOykv+cVPdiXH5M=
github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc=
github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI=
github.com/catenacyber/perfsprint v0.9.1 h1:5LlTp4RwTooQjJCvGEFV6XksZvWE7wCOUvjD2z0vls0=
github.com/catenacyber/perfsprint v0.9.1/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM=
github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg=
github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4=
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc=
github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww=
github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=
github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=
github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=
github.com/daixiang0/gci v0.13.6 h1:RKuEOSkGpSadkGbvZ6hJ4ddItT3cVZ9Vn9Rybk6xjl8=
github.com/daixiang0/gci v0.13.6/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk=
github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY=
github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk=
github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE=
github.com/elastic/crd-ref-docs v0.1.0 h1:Cr5kz89QB3Iuuj7dhAfLMApCrChEGAaIBTxGk/xuRKw=
github.com/elastic/crd-ref-docs v0.1.0/go.mod h1:X83mMBdJt05heJUYiS3T0yJ/JkCuliuhSUNav5Gjo/U=
github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=