Compare commits

...

11 commits
v0.4.1 ... main

Author SHA1 Message Date
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
76 changed files with 4166 additions and 3470 deletions

View file

@ -28,4 +28,4 @@ jobs:
- name: Run linter
uses: golangci/golangci-lint-action@v6
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

@ -36,7 +36,6 @@ jobs:
- name: Init go
run: |
go mod download
go mod download -modfile tools/go.mod
- name: Snapshot release
uses: goreleaser/goreleaser-action@v6

View file

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

View file

@ -1,36 +1,18 @@
version: "2"
run:
timeout: 5m
allow-parallel-runners: true
issues:
# don't skip warning about doc comments
# don't exclude the default set of lint
exclude-use-default: false
# restore some of the defaults
# (fill in the rest as needed)
exclude-rules:
- path: "api/*"
linters:
- lll
- path: "internal/*"
linters:
- dupl
- lll
linters:
disable-all: true
default: none
enable:
- copyloopvar
- dupl
- errcheck
- copyloopvar
- ginkgolinter
- goconst
- gocyclo
- gofmt
- goimports
- goheader
- gosimple
# enable when the TODOs are fixed
# - godox
- goheader
- govet
- ineffassign
- lll
@ -39,43 +21,65 @@ linters:
- prealloc
- revive
- staticcheck
- typecheck
- unconvert
- unparam
- unused
linters-settings:
revive:
settings:
goheader:
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:
- name: comment-spacings
gci:
sections:
- standard
- default
- prefix(code.icb4dc0.de/prskr/supabase-operator)
- alias
- blank
- dot
goimports:
local-prefixes: code.icb4dc0.de/prskr/supabase-operator
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
- linters:
- lll
path: api/*
- linters:
- dupl
- lll
path: internal/*
paths:
- third_party$
- builtin$
- examples$
severity:
default-severity: error
default: error
rules:
- linters:
- godox
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

@ -2,7 +2,7 @@
# git hook 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",
# "golangci-lint run",

View file

@ -4,6 +4,19 @@
"initialization_options": {
"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"
]
}
}
}
}

View file

@ -1,5 +1,5 @@
# Build the manager binary
FROM golang:1.23.4 AS builder
FROM golang:1.24-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
@ -16,10 +16,7 @@ COPY [ "go.*", "./" ]
COPY [ "api", "api" ]
COPY [ "assets/migrations", "assets/migrations" ]
COPY [ "cmd", "cmd" ]
COPY [ "infrastructure", "infrastructure" ]
COPY [ "internal", "internal" ]
COPY [ "magefiles", "magefiles" ]
COPY [ "tools", "tools" ]
# Build
# 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
.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
.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="./..."
.PHONY: fmt
@ -60,7 +60,7 @@ vet: ## Run go vet against code.
go vet ./...
.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
# 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
.PHONY: lint
lint: golangci-lint ## Run golangci-lint linter
lint: ## Run golangci-lint linter
$(GOLANGCI_LINT) run
.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
##@ Build
@ -127,7 +127,7 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform
rm Dockerfile.cross
.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
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default > dist/install.yaml
@ -148,7 +148,7 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified
.PHONY: deploy
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 -
.PHONY: undeploy
@ -164,49 +164,7 @@ $(LOCALBIN):
## Tool Binaries
KUBECTL ?= kubectl
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
## Tool Versions
KUSTOMIZE_VERSION ?= v5.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
KUSTOMIZE ?= go tool kustomize
CONTROLLER_GEN ?= go tool controller-gen
ENVTEST ?= go tool setup-envtest
GOLANGCI_LINT = go tool golangci-lint

View file

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

View file

@ -22,6 +22,8 @@ local_resource(
resource_deps=[]
)
k8s_kind('Cluster', api_version='postgresql.cnpg.io/v1')
docker_build_with_restart(
'supabase-operator',
'.',
@ -39,10 +41,11 @@ k8s_resource('supabase-controller-manager')
k8s_resource(
workload='supabase-control-plane',
port_forwards=18000,
resource_deps=[]
)
k8s_resource(
objects=["cluster-example:Cluster:supabase-demo"],
workload='cluster-example',
new_name='Postgres cluster',
port_forwards=5432
)
@ -62,6 +65,11 @@ k8s_resource(
objects=["gateway-sample:APIGateway:supabase-demo"],
extra_pod_selectors={"app.kubernetes.io/component": "api-gateway"},
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',
resource_deps=[
'supabase-controller-manager'

View file

@ -80,7 +80,7 @@ type EnvoySpec struct {
// ControlPlane - configure the control plane where Envoy will retrieve its configuration from
ControlPlane *ControlPlaneSpec `json:"controlPlane"`
// 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"`
@ -128,10 +128,13 @@ const (
)
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"`
TokenEndpoint string `json:"tokenEndpoint,omitempty"`
// AuthorizationEndpoint - endpoint where the user will be redirected to authenticate
AuthorizationEndpoint string `json:"authorizationEndpoint"`
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
@ -142,11 +145,24 @@ type DashboardOAuth2Spec struct {
ClientSecretRef *corev1.SecretKeySelector `json:"clientSecretRef"`
}
type DashboardBasicAuthSpec struct{}
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 *DashboardOAuth2Spec `json:"oauth2,omitempty"`
Basic *DashboardBasicAuthSpec `json:"basic,omitempty"`
// 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 {
@ -188,6 +204,14 @@ func (s *DashboardEndpointSpec) OAuth2() *DashboardOAuth2Spec {
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.
type APIGatewaySpec struct {
// Envoy - configure the envoy instance and most importantly the control-plane

View file

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

View file

@ -17,6 +17,7 @@ limitations under the License.
package v1alpha1
import (
"bytes"
"context"
"errors"
"fmt"
@ -25,6 +26,7 @@ import (
"slices"
"strconv"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -59,7 +61,7 @@ type DatabaseRoles struct {
type Database struct {
DSN *string `json:"dsn,omitempty"`
DSNSecretRef *corev1.SecretKeySelector `json:"dsnSecretRef,omitempty"`
DSNSecretRef *corev1.SecretKeySelector `json:"dsnSecretRef"`
Roles DatabaseRoles `json:"roles,omitempty"`
}
@ -167,8 +169,8 @@ type PostgrestSpec struct {
// MaxRows - maximum number of rows PostgREST will load at a time
// +kubebuilder:default=1000
MaxRows int `json:"maxRows,omitempty"`
// WorkloadTemplate - customize the PostgREST workload
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"`
// WorkloadSpec - customize the PostgREST workload
WorkloadSpec *WorkloadSpec `json:"workloadSpec,omitempty"`
}
type AuthProviderMeta struct {
@ -365,12 +367,12 @@ func (p *AuthProviders) Vars(apiExternalURL string) []corev1.EnvVar {
}
type AuthSpec struct {
AdditionalRedirectUrls []string `json:"additionalRedirectUrls,omitempty"`
DisableSignup *bool `json:"disableSignup,omitempty"`
AnonymousUsersEnabled *bool `json:"anonymousUsersEnabled,omitempty"`
Providers *AuthProviders `json:"providers,omitempty"`
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"`
EmailSignupDisabled *bool `json:"emailSignupDisabled,omitempty"`
AdditionalRedirectUrls []string `json:"additionalRedirectUrls,omitempty"`
DisableSignup *bool `json:"disableSignup,omitempty"`
AnonymousUsersEnabled *bool `json:"anonymousUsersEnabled,omitempty"`
Providers *AuthProviders `json:"providers,omitempty"`
WorkloadTemplate *WorkloadSpec `json:"workloadTemplate,omitempty"`
EmailSignupDisabled *bool `json:"emailSignupDisabled,omitempty"`
}
// CoreSpec defines the desired state of Core.
@ -387,20 +389,89 @@ type CoreSpec struct {
Auth *AuthSpec `json:"auth,omitempty"`
}
type MigrationStatus map[string]metav1.Time
type MigrationConditionStatus string
func (s MigrationStatus) IsApplied(name string) bool {
_, ok := s[name]
return ok
}
const (
MigrationConditionStatusApplied MigrationConditionStatus = "Applied"
MigrationConditionStatusFailed MigrationConditionStatus = "Failed"
)
func (s MigrationStatus) Record(name string) {
s[name] = metav1.Now()
type MigrationScriptCondition struct {
// 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 {
AppliedMigrations MigrationStatus `json:"appliedMigrations,omitempty"`
Roles map[string][]byte `json:"roles,omitempty"`
MigrationConditions []MigrationScriptCondition `json:"migrationConditions,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
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

View file

@ -24,7 +24,7 @@ import (
type StudioSpec struct {
JWT *JwtSpec `json:"jwt,omitempty"`
// 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
// 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
@ -37,7 +37,7 @@ type StudioSpec struct {
type PGMetaSpec struct {
// WorkloadTemplate - customize the pg-meta deployment
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"`
WorkloadSpec *WorkloadSpec `json:"workloadSpec,omitempty"`
}
type DbCredentialsReference struct {

View file

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

View file

@ -21,7 +21,7 @@ limitations under the License.
package v1alpha1
import (
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@ -252,7 +252,7 @@ func (in *AuthSpec) DeepCopyInto(out *AuthSpec) {
}
if in.WorkloadTemplate != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
*out = new(WorkloadTemplate)
*out = new(WorkloadSpec)
(*in).DeepCopyInto(*out)
}
if in.EmailSignupDisabled != nil {
@ -506,7 +506,7 @@ func (in *DashboardAuthSpec) DeepCopyInto(out *DashboardAuthSpec) {
if in.Basic != nil {
in, out := &in.Basic, &out.Basic
*out = new(DashboardBasicAuthSpec)
**out = **in
(*in).DeepCopyInto(*out)
}
}
@ -523,6 +523,11 @@ func (in *DashboardAuthSpec) DeepCopy() *DashboardAuthSpec {
// 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.
@ -747,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.
func (in *DatabaseStatus) DeepCopyInto(out *DatabaseStatus) {
*out = *in
if in.AppliedMigrations != nil {
in, out := &in.AppliedMigrations, &out.AppliedMigrations
*out = make(MigrationStatus, len(*in))
for key, val := range *in {
(*out)[key] = *val.DeepCopy()
if in.MigrationConditions != nil {
in, out := &in.MigrationConditions, &out.MigrationConditions
*out = make([]MigrationScriptCondition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Roles != nil {
@ -916,9 +921,9 @@ func (in *EnvoySpec) DeepCopyInto(out *EnvoySpec) {
*out = new(ControlPlaneSpec)
**out = **in
}
if in.WorkloadTemplate != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
*out = new(WorkloadTemplate)
if in.WorkloadSpec != nil {
in, out := &in.WorkloadSpec, &out.WorkloadSpec
*out = new(WorkloadSpec)
(*in).DeepCopyInto(*out)
}
if in.Debugging != nil {
@ -993,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.
func (in *ImageProxySpec) DeepCopyInto(out *ImageProxySpec) {
*out = *in
if in.WorkloadTemplate != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
*out = new(WorkloadTemplate)
if in.WorkloadSpec != nil {
in, out := &in.WorkloadSpec, &out.WorkloadSpec
*out = new(WorkloadSpec)
(*in).DeepCopyInto(*out)
}
}
@ -1041,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.
func (in MigrationStatus) DeepCopyInto(out *MigrationStatus) {
{
in := &in
*out = make(MigrationStatus, len(*in))
for key, val := range *in {
(*out)[key] = *val.DeepCopy()
}
func (in *MigrationScriptCondition) DeepCopyInto(out *MigrationScriptCondition) {
*out = *in
if in.Hash != nil {
in, out := &in.Hash, &out.Hash
*out = make([]byte, len(*in))
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.
func (in MigrationStatus) DeepCopy() MigrationStatus {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MigrationScriptCondition.
func (in *MigrationScriptCondition) DeepCopy() *MigrationScriptCondition {
if in == nil {
return nil
}
out := new(MigrationStatus)
out := new(MigrationScriptCondition)
in.DeepCopyInto(out)
return *out
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
@ -1084,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.
func (in *PGMetaSpec) DeepCopyInto(out *PGMetaSpec) {
*out = *in
if in.WorkloadTemplate != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
*out = new(WorkloadTemplate)
if in.WorkloadSpec != nil {
in, out := &in.WorkloadSpec, &out.WorkloadSpec
*out = new(WorkloadSpec)
(*in).DeepCopyInto(*out)
}
}
@ -1130,9 +1136,9 @@ func (in *PostgrestSpec) DeepCopyInto(out *PostgrestSpec) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.WorkloadTemplate != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
*out = new(WorkloadTemplate)
if in.WorkloadSpec != nil {
in, out := &in.WorkloadSpec, &out.WorkloadSpec
*out = new(WorkloadSpec)
(*in).DeepCopyInto(*out)
}
}
@ -1289,9 +1295,9 @@ func (in *StorageApiSpec) DeepCopyInto(out *StorageApiSpec) {
*out = new(UploadTempSpec)
(*in).DeepCopyInto(*out)
}
if in.WorkloadTemplate != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
*out = new(WorkloadTemplate)
if in.WorkloadSpec != nil {
in, out := &in.WorkloadSpec, &out.WorkloadSpec
*out = new(WorkloadSpec)
(*in).DeepCopyInto(*out)
}
}
@ -1382,9 +1388,9 @@ func (in *StudioSpec) DeepCopyInto(out *StudioSpec) {
*out = new(JwtSpec)
**out = **in
}
if in.WorkloadTemplate != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
*out = new(WorkloadTemplate)
if in.WorkloadSpec != nil {
in, out := &in.WorkloadSpec, &out.WorkloadSpec
*out = new(WorkloadSpec)
(*in).DeepCopyInto(*out)
}
if in.GatewayServiceMatchLabels != nil {
@ -1442,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.
func (in *WorkloadTemplate) DeepCopyInto(out *WorkloadTemplate) {
func (in *WorkloadSpec) DeepCopyInto(out *WorkloadSpec) {
*out = *in
if in.Replicas != nil {
in, out := &in.Replicas, &out.Replicas
@ -1461,8 +1467,8 @@ func (in *WorkloadTemplate) DeepCopyInto(out *WorkloadTemplate) {
(*out)[key] = val
}
}
if in.Workload != nil {
in, out := &in.Workload, &out.Workload
if in.ContainerSpec != nil {
in, out := &in.ContainerSpec, &out.ContainerSpec
*out = new(ContainerTemplate)
(*in).DeepCopyInto(*out)
}
@ -1475,12 +1481,12 @@ func (in *WorkloadTemplate) DeepCopyInto(out *WorkloadTemplate) {
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadTemplate.
func (in *WorkloadTemplate) DeepCopy() *WorkloadTemplate {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadSpec.
func (in *WorkloadSpec) DeepCopy() *WorkloadSpec {
if in == nil {
return nil
}
out := new(WorkloadTemplate)
out := new(WorkloadSpec)
in.DeepCopyInto(out)
return out
}

View file

@ -17,6 +17,7 @@ limitations under the License.
package migrations
import (
"crypto/sha256"
"embed"
"fmt"
"io/fs"
@ -32,6 +33,7 @@ var migrationsFS embed.FS
type Script struct {
FileName string
Content string
Hash []byte
}
func InitScripts() iter.Seq2[Script, error] {
@ -49,10 +51,18 @@ func RoleCreationScript(roleName string) (Script, error) {
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] {
hash := sha256.New()
return func(yield func(Script, error) bool) {
files, err := migrationsFS.ReadDir(dir)
if err != nil {
@ -76,11 +86,16 @@ func readScripts(dir string) iter.Seq2[Script, error] {
}
}
_, _ = hash.Write(content)
s := Script{
FileName: file.Name(),
Content: string(content),
Hash: hash.Sum(nil),
}
hash.Reset()
if !yield(s, nil) {
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;
vault_exists boolean;
BEGIN
pgsodium_exists = (
select count(*) = 1
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 = (
IF EXISTS (SELECT FROM pg_available_extensions WHERE name = 'supabase_vault' AND default_version != '0.2.8') 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;
ELSE
pgsodium_exists = (
select count(*) = 1
from pg_available_extensions
where name = 'supabase_vault'
);
IF pgsodium_exists
THEN
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
where name = 'pgsodium'
and default_version in ('3.1.6', '3.1.7', '3.1.8', '3.1.9')
);
vault_exists = (
select count(*) = 1
from pg_available_extensions
where name = 'supabase_vault'
);
IF pgsodium_exists
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 $$;

View file

@ -4,5 +4,12 @@ ALTER ROLE authenticated inherit;
ALTER ROLE anon inherit;
ALTER ROLE service_role inherit;
DO $$
BEGIN
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'pgsodium_keyholder') THEN
GRANT pgsodium_keyholder to service_role;
END IF;
END $$;
-- 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,3 @@
create schema if not exists _realtime;
alter schema _realtime owner to supabase_admin;

View file

@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.1
controller-gen.kubebuilder.io/version: v0.17.2
name: apigateways.supabase.k8s.icb4dc0.de
spec:
group: supabase.k8s.icb4dc0.de
@ -121,8 +121,28 @@ spec:
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
@ -157,6 +177,11 @@ spec:
- 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
@ -174,10 +199,8 @@ spec:
retrieve the OAuth2 access and identity token from
type: string
required:
- authorizationEndpoint
- clientId
- clientSecretRef
- tokenEndpoint
type: object
type: object
tls:
@ -272,7 +295,7 @@ spec:
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.
type: string
workloadTemplate:
workloadSpec:
description: WorkloadTemplate - customize the Envoy deployment
properties:
additionalLabels:
@ -2079,248 +2102,9 @@ spec:
- name
type: object
type: array
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
workload:
description: Workload - customize the container template of
the workload
container:
description: ContainerSpec - customize the container template
of the workload
properties:
additionalEnv:
items:
@ -2785,6 +2569,245 @@ spec:
type: object
type: array
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
required:
- controlPlane

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

@ -9,19 +9,20 @@ metadata:
namespace: supabase-demo
spec:
envoy:
debugging:
componentLogLevels:
- component: oauth2
level: debug
disableIPv6: true
workloadSpec:
replicas: 2
apiEndpoint:
jwks:
name: core-sample-jwt
key: jwks.json
dashboardEndpoint:
tls:
cert:
secretName: dashboard-tls-cert
auth:
oauth2:
tokenEndpoint: "https://login.microsoftonline.com/f4e80111-1571-477a-b56d-c5fe517676b7/oauth2/token"
authorizationEndpoint: "https://login.microsoftonline.com/f4e80111-1571-477a-b56d-c5fe517676b7/oauth2/authorize"
openIdIssuer: "https://login.microsoftonline.com/f4e80111-1571-477a-b56d-c5fe517676b7/"
clientId: 3528016b-f6e3-49be-8fb3-f9a9a2ab6c3f
scopes:
- openid
@ -49,6 +50,6 @@ spec:
- gateway-sample-envoy.supabase-demo.svc.cluster.local
- localhost:3000
issuerRef:
kind: Issuer
name: selfsigned-issuer
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

@ -44,14 +44,15 @@ metadata:
namespace: supabase-demo
spec:
instances: 1
imageName: ghcr.io/supabase/postgres:15.8.1.021
postgresUID: 105
postgresGID: 106
imageName: code.icb4dc0.de/prskr/supabase-operator/postgres:17.2
imagePullPolicy: Always
postgresUID: 26
postgresGID: 102
bootstrap:
initdb:
database: app
owner: setup
owner: supabase_admin
postInitSQL:
- drop publication if exists supabase_realtime;

View file

@ -1,3 +1,4 @@
---
apiVersion: v1
kind: Secret
metadata:
@ -25,6 +26,8 @@ spec:
# 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

View file

@ -2,12 +2,14 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
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
- ca.yaml
- namespace.yaml
- cnpg-cluster.yaml
- minio.yaml
- ../default
- studio-plaintext-users.yaml
- studio-credentials-secret.yaml
- core.yaml
- apigateway.yaml
@ -23,3 +25,6 @@ patches:
target:
kind: Deployment
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

@ -25,7 +25,7 @@ spec:
bucket: test
credentialsSecretRef:
secretName: storage-s3-credentials
s3Protocol: {}
s3: {}
db:
host: cluster-example-rw.supabase-demo.svc
dbName: app

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

@ -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

@ -15,7 +15,7 @@ spec:
bucket: test
credentialsSecretRef:
secretName: storage-s3-credentials
s3Protocol: {}
s3: {}
db:
host: cluster-example-rw.supabase-demo.svc
dbName: app

View file

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

View file

@ -151,7 +151,7 @@ _Appears in:_
| `disableSignup` _boolean_ | | | |
| `anonymousUsersEnabled` _boolean_ | | | |
| `providers` _[AuthProviders](#authproviders)_ | | | |
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | | | |
| `workloadTemplate` _[WorkloadSpec](#workloadspec)_ | | | |
| `emailSignupDisabled` _boolean_ | | | |
@ -185,7 +185,7 @@ _Appears in:_
_Appears in:_
- [WorkloadTemplate](#workloadtemplate)
- [WorkloadSpec](#workloadspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
@ -331,8 +331,8 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `oauth2` _[DashboardOAuth2Spec](#dashboardoauth2spec)_ | | | |
| `basic` _[DashboardBasicAuthSpec](#dashboardbasicauthspec)_ | | | |
| `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 | | |
@ -348,6 +348,10 @@ _Appears in:_
_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
@ -417,6 +421,7 @@ _Appears in:_
| 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 | | |
@ -513,7 +518,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `appliedMigrations` _[MigrationStatus](#migrationstatus)_ | | | |
| `migrationConditions` _[MigrationScriptCondition](#migrationscriptcondition) array_ | | | |
| `roles` _object (keys:string, values:integer array)_ | | | |
@ -655,7 +660,7 @@ _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. | | |
| `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)_ | | | |
@ -726,7 +731,7 @@ _Appears in:_
| --- | --- | --- | --- |
| `enable` _boolean_ | Enable - whether to deploy the image proxy or not | | |
| `enableWebPDetection` _boolean_ | | | |
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the image proxy workload | | |
| `workloadSpec` _[WorkloadSpec](#workloadspec)_ | WorkloadTemplate - customize the image proxy workload | | |
#### ImageSpec
@ -768,9 +773,11 @@ _Appears in:_
| `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
@ -779,6 +786,14 @@ _Underlying type:_ _[Time](https://kubernetes.io/docs/reference/generated/kubern
_Appears in:_
- [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
@ -813,7 +828,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the pg-meta deployment | | |
| `workloadSpec` _[WorkloadSpec](#workloadspec)_ | WorkloadTemplate - customize the pg-meta deployment | | |
#### PhoneAuthProvider
@ -849,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] | |
| `anonRole` _string_ | AnonRole - name of the anon role | anon | |
| `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
@ -984,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 | | |
| `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 | | |
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the Storage API workload | | |
| `workloadSpec` _[WorkloadSpec](#workloadspec)_ | WorkloadTemplate - customize the Storage API workload | | |
#### StorageList
@ -1038,7 +1053,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `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 \} | |
| `externalUrl` _string_ | APIExternalURL is referring to the URL where Supabase API will be available<br />Typically this is the ingress of the API gateway | | |
@ -1079,7 +1094,7 @@ _Appears in:_
| `sizeLimit` _[Quantity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#quantity-resource-api)_ | | | |
#### WorkloadTemplate
#### WorkloadSpec
@ -1101,7 +1116,7 @@ _Appears in:_
| `replicas` _integer_ | | | |
| `securityContext` _[PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#podsecuritycontext-v1-core)_ | | | |
| `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_ | | | |

239
go.mod
View file

@ -1,6 +1,8 @@
module code.icb4dc0.de/prskr/supabase-operator
go 1.23.5
go 1.24
toolchain go1.24.0
require (
github.com/alecthomas/kong v1.7.0
@ -12,10 +14,11 @@ require (
github.com/magefile/mage v1.15.0
github.com/onsi/ginkgo/v2 v2.22.2
github.com/onsi/gomega v1.36.2
github.com/stretchr/testify v1.10.0
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.4
google.golang.org/protobuf v1.36.6
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.32.1
k8s.io/apimachinery v0.32.1
@ -23,67 +26,254 @@ require (
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 (
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
4d63.com/gochecknoglobals v0.2.2 // 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/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/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/cespare/xxhash/v2 v2.3.0 // 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/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/envoyproxy/go-control-plane/ratelimit v0.1.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/firefart/nonamedreturns v1.0.5 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // 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/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/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/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/cel-go v0.22.0 // 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/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/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/jackc/pgpassfile v1.0.0 // 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/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/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/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect
github.com/lestrrat-go/iter v1.0.2 // 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/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/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/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