feat: basic functionality implemented
- added Core CRD to manage DB migrations & configuration, PostgREST and GoTrue (auth) - added APIGateway CRD to manage Envoy proxy - added Dashboard CRD to manage (so far) pg-meta and (soon) studio deployments - implemented basic Envoy control plane based on K8s watcher
This commit is contained in:
parent
2fae578618
commit
647f602c79
123 changed files with 12173 additions and 581 deletions
|
@ -1,3 +1,7 @@
|
||||||
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||||
# Ignore build and test binaries.
|
# Ignore build and test binaries.
|
||||||
bin/
|
.idea
|
||||||
|
.devcontainer
|
||||||
|
.github
|
||||||
|
.zed
|
||||||
|
hack
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1 +1,4 @@
|
||||||
bin/
|
bin/
|
||||||
|
out/
|
||||||
|
.idea/
|
||||||
|
.venv/
|
||||||
|
|
|
@ -28,6 +28,7 @@ linters:
|
||||||
- gofmt
|
- gofmt
|
||||||
- goimports
|
- goimports
|
||||||
- gosimple
|
- gosimple
|
||||||
|
- godox
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- lll
|
- lll
|
||||||
|
|
27
Dockerfile
27
Dockerfile
|
@ -1,33 +1,38 @@
|
||||||
# Build the manager binary
|
# Build the manager binary
|
||||||
FROM golang:1.22 AS builder
|
FROM golang:1.23.4 AS builder
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
# Copy the Go Modules manifests
|
|
||||||
COPY go.mod go.mod
|
|
||||||
COPY go.sum go.sum
|
|
||||||
# cache deps before building and copying source so that we don't need to re-download as much
|
# cache deps before building and copying source so that we don't need to re-download as much
|
||||||
# and so that source changes don't invalidate our downloaded layer
|
# and so that source changes don't invalidate our downloaded layer
|
||||||
RUN go mod download
|
RUN --mount=type=bind,source=go.mod,target=go.mod \
|
||||||
|
--mount=type=bind,source=go.sum,target=go.sum \
|
||||||
|
go mod download
|
||||||
|
|
||||||
# Copy the go source
|
# Copy the go source
|
||||||
COPY cmd/main.go cmd/main.go
|
COPY [ "go.*", "./" ]
|
||||||
COPY api/ api/
|
COPY [ "api", "api" ]
|
||||||
COPY internal/ internal/
|
COPY [ "assets/migrations", "assets/migrations" ]
|
||||||
|
COPY [ "cmd", "cmd" ]
|
||||||
|
COPY [ "infrastructure", "infrastructure" ]
|
||||||
|
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
|
||||||
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
|
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
|
||||||
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
|
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
|
||||||
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
|
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
|
||||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
|
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o supabase-operator ./cmd/
|
||||||
|
|
||||||
# Use distroless as minimal base image to package the manager binary
|
# Use distroless as minimal base image to package the manager binary
|
||||||
# Refer to https://github.com/GoogleContainerTools/distroless for more details
|
# Refer to https://github.com/GoogleContainerTools/distroless for more details
|
||||||
FROM gcr.io/distroless/static:nonroot
|
FROM gcr.io/distroless/static:nonroot
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY --from=builder /workspace/manager .
|
COPY --from=builder /workspace/supabase-operator .
|
||||||
USER 65532:65532
|
USER 65532:65532
|
||||||
|
|
||||||
ENTRYPOINT ["/manager"]
|
ENTRYPOINT ["/supabase-operator"]
|
||||||
|
|
26
PROJECT
26
PROJECT
|
@ -17,4 +17,30 @@ resources:
|
||||||
kind: Core
|
kind: Core
|
||||||
path: code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1
|
path: code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1
|
||||||
version: v1alpha1
|
version: v1alpha1
|
||||||
|
webhooks:
|
||||||
|
defaulting: true
|
||||||
|
validation: true
|
||||||
|
webhookVersion: v1
|
||||||
|
- api:
|
||||||
|
crdVersion: v1
|
||||||
|
namespaced: true
|
||||||
|
controller: true
|
||||||
|
domain: k8s.icb4dc0.de
|
||||||
|
group: supabase
|
||||||
|
kind: APIGateway
|
||||||
|
path: code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1
|
||||||
|
version: v1alpha1
|
||||||
|
webhooks:
|
||||||
|
defaulting: true
|
||||||
|
validation: true
|
||||||
|
webhookVersion: v1
|
||||||
|
- api:
|
||||||
|
crdVersion: v1
|
||||||
|
namespaced: true
|
||||||
|
controller: true
|
||||||
|
domain: k8s.icb4dc0.de
|
||||||
|
group: supabase
|
||||||
|
kind: Dashboard
|
||||||
|
path: code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1
|
||||||
|
version: v1alpha1
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|
69
Tiltfile
Normal file
69
Tiltfile
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# -*- mode: Python -*-
|
||||||
|
load('ext://restart_process', 'docker_build_with_restart')
|
||||||
|
|
||||||
|
allow_k8s_contexts('kind-kind')
|
||||||
|
|
||||||
|
local('./dev/prepare-dev-cluster.sh')
|
||||||
|
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/'
|
||||||
|
|
||||||
|
local_resource(
|
||||||
|
'manager-go-compile',
|
||||||
|
compile_cmd,
|
||||||
|
deps=[
|
||||||
|
'./api',
|
||||||
|
'./cmd',
|
||||||
|
'./assets',
|
||||||
|
'./infrastructure',
|
||||||
|
'./internal',
|
||||||
|
'./go.mod',
|
||||||
|
'./go.sum'
|
||||||
|
],
|
||||||
|
resource_deps=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
docker_build_with_restart(
|
||||||
|
'supabase-operator',
|
||||||
|
'.',
|
||||||
|
entrypoint=['/app/bin/supabase-operator'],
|
||||||
|
dockerfile='dev/Dockerfile',
|
||||||
|
only=[
|
||||||
|
'./out',
|
||||||
|
],
|
||||||
|
live_update=[
|
||||||
|
sync('./out', '/app/bin'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
k8s_resource('supabase-controller-manager')
|
||||||
|
k8s_resource(
|
||||||
|
workload='supabase-control-plane',
|
||||||
|
port_forwards=18000,
|
||||||
|
)
|
||||||
|
|
||||||
|
k8s_resource(
|
||||||
|
objects=["cluster-example:Cluster:supabase-demo"],
|
||||||
|
new_name='Postgres cluster',
|
||||||
|
port_forwards=5432
|
||||||
|
)
|
||||||
|
|
||||||
|
k8s_resource(
|
||||||
|
objects=["core-sample:Core:supabase-demo"],
|
||||||
|
new_name='Supabase Core',
|
||||||
|
resource_deps=[
|
||||||
|
'Postgres cluster',
|
||||||
|
'supabase-controller-manager'
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
k8s_resource(
|
||||||
|
objects=["core-sample:APIGateway:supabase-demo"],
|
||||||
|
extra_pod_selectors={"app.kubernetes.io/component": "api-gateway"},
|
||||||
|
port_forwards=[8000, 19000],
|
||||||
|
new_name='API Gateway',
|
||||||
|
resource_deps=[
|
||||||
|
'supabase-controller-manager'
|
||||||
|
],
|
||||||
|
)
|
13
api/common.go
Normal file
13
api/common.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"iter"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ObjectList[T metav1.Object] interface {
|
||||||
|
client.ObjectList
|
||||||
|
Iter() iter.Seq[T]
|
||||||
|
}
|
95
api/v1alpha1/apigateway_types.go
Normal file
95
api/v1alpha1/apigateway_types.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"iter"
|
||||||
|
"maps"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(&APIGateway{}, &APIGatewayList{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlPlaneSpec struct {
|
||||||
|
// Host is the hostname of the envoy control plane endpoint
|
||||||
|
Host string `json:"host"`
|
||||||
|
// Port is the port number of the envoy control plane endpoint - typically this is 18000
|
||||||
|
// +kubebuilder:default=18000
|
||||||
|
// +kubebuilder:validation:Maximum=65535
|
||||||
|
Port uint16 `json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIGatewaySpec defines the desired state of APIGateway.
|
||||||
|
type APIGatewaySpec struct {
|
||||||
|
// Envoy - configure the envoy instance and most importantly the control-plane
|
||||||
|
Envoy *EnvoySpec `json:"envoy"`
|
||||||
|
// JWKSSelector - selector where the JWKS can be retrieved from to enable the API gateway to validate JWTs
|
||||||
|
JWKSSelector *corev1.SecretKeySelector `json:"jwks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIGatewayStatus defines the observed state of APIGateway.
|
||||||
|
type APIGatewayStatus struct{}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
// +kubebuilder:subresource:status
|
||||||
|
|
||||||
|
// APIGateway is the Schema for the apigateways API.
|
||||||
|
type APIGateway struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Spec APIGatewaySpec `json:"spec,omitempty"`
|
||||||
|
Status APIGatewayStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g APIGateway) JwksSecretMeta() metav1.ObjectMeta {
|
||||||
|
return metav1.ObjectMeta{
|
||||||
|
Name: g.Spec.JWKSSelector.Name,
|
||||||
|
Namespace: g.Namespace,
|
||||||
|
Labels: maps.Clone(g.Labels),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
|
||||||
|
// APIGatewayList contains a list of APIGateway.
|
||||||
|
type APIGatewayList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
Items []APIGateway `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l APIGatewayList) Iter() iter.Seq[*APIGateway] {
|
||||||
|
return func(yield func(*APIGateway) bool) {
|
||||||
|
for _, gw := range l.Items {
|
||||||
|
if !yield(&gw) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
api/v1alpha1/common_types.go
Normal file
44
api/v1alpha1/common_types.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImageSpec struct {
|
||||||
|
Image string `json:"image,omitempty"`
|
||||||
|
PullPolicy corev1.PullPolicy `json:"pullPolicy,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerTemplate struct {
|
||||||
|
ImageSpec `json:",inline"`
|
||||||
|
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
|
||||||
|
// SecurityContext -
|
||||||
|
SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"`
|
||||||
|
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
|
||||||
|
VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"`
|
||||||
|
AdditionalEnv []corev1.EnvVar `json:"additionalEnv,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkloadTemplate struct {
|
||||||
|
Replicas *int32 `json:"replicas,omitempty"`
|
||||||
|
SecurityContext *corev1.PodSecurityContext `json:"securityContext"`
|
||||||
|
AdditionalLabels map[string]string `json:"additionalLabels,omitempty"`
|
||||||
|
// Workload - customize the container template of the workload
|
||||||
|
Workload *ContainerTemplate `json:"workload,omitempty"`
|
||||||
|
}
|
|
@ -19,46 +19,366 @@ package v1alpha1
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"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"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(&Core{}, &CoreList{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNoSuchSecretValue = errors.New("no such secret value")
|
||||||
|
|
||||||
|
type DatabaseRolesSecrets struct {
|
||||||
|
Admin *corev1.LocalObjectReference `json:"supabaseAdmin,omitempty"`
|
||||||
|
Authenticator *corev1.LocalObjectReference `json:"authenticator,omitempty"`
|
||||||
|
AuthAdmin *corev1.LocalObjectReference `json:"supabaseAuthAdmin,omitempty"`
|
||||||
|
FunctionsAdmin *corev1.LocalObjectReference `json:"supabaseFunctionsAdmin,omitempty"`
|
||||||
|
StorageAdmin *corev1.LocalObjectReference `json:"supabaseStorageAdmin,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatabaseRoles struct {
|
||||||
|
// SelfManaged - whether the database roles are managed externally
|
||||||
|
// when enabled the operator does not attempt to create secrets, generate passwords or whatsoever for all database roles
|
||||||
|
// i.e. all secrets need to be provided or the instance won't work
|
||||||
|
SelfManaged bool `json:"selfManaged,omitempty"`
|
||||||
|
// Secrets - typed 'map' of secrets for each database role that Supabase needs
|
||||||
|
Secrets DatabaseRolesSecrets `json:"secrets,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type Database struct {
|
type Database struct {
|
||||||
DSN *string `json:"dsn,omitempty"`
|
DSN *string `json:"dsn,omitempty"`
|
||||||
DSNFrom *corev1.SecretKeySelector `json:"dsnFrom,omitempty"`
|
DSNSecretRef *corev1.SecretKeySelector `json:"dsnFrom,omitempty"`
|
||||||
|
Roles DatabaseRoles `json:"roles,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Database) GetDSN(ctx context.Context, client client.Client) (string, error) {
|
func (d Database) GetDSN(ctx context.Context, client client.Client) (string, error) {
|
||||||
if d.DSN != nil {
|
if d.DSNSecretRef == nil {
|
||||||
return *d.DSN, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.DSNFrom == nil {
|
|
||||||
return "", errors.New("DSN not set")
|
return "", errors.New("DSN not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
var secret corev1.Secret
|
var secret corev1.Secret
|
||||||
if err := client.Get(ctx, types.NamespacedName{Name: d.DSNFrom.Name}, &secret); err != nil {
|
if err := client.Get(ctx, types.NamespacedName{Name: d.DSNSecretRef.Name}, &secret); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, ok := secret.Data[d.DSNFrom.Key]
|
data, ok := secret.Data[d.DSNSecretRef.Key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", errors.New("key not found in secret")
|
return "", fmt.Errorf("%w: %s", ErrNoSuchSecretValue, d.DSNSecretRef.Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(data), nil
|
return string(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d Database) DSNEnv(key string) corev1.EnvVar {
|
||||||
|
return corev1.EnvVar{
|
||||||
|
Name: key,
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: d.DSNSecretRef,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type JwtSpec struct {
|
||||||
|
// Secret - JWT HMAC secret in plain text
|
||||||
|
// This is WRITE-ONLY and will be copied to the SecretRef by the defaulter
|
||||||
|
Secret *string `json:"secret,omitempty"`
|
||||||
|
// SecretRef - object reference to the Secret where JWT values are stored
|
||||||
|
SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"`
|
||||||
|
// SecretKey - key in secret where to read the JWT HMAC secret from
|
||||||
|
// +kubebuilder:default=secret
|
||||||
|
SecretKey string `json:"secretKey,omitempty"`
|
||||||
|
// JwksKey - key in secret where to read the JWKS from
|
||||||
|
// +kubebuilder:default=jwks.json
|
||||||
|
JwksKey string `json:"jwksKey,omitempty"`
|
||||||
|
// AnonKey - key in secret where to read the anon JWT from
|
||||||
|
// +kubebuilder:default=anon_key
|
||||||
|
AnonKey string `json:"anonKey,omitempty"`
|
||||||
|
// ServiceKey - key in secret where to read the service JWT from
|
||||||
|
// +kubebuilder:default=service_key
|
||||||
|
ServiceKey string `json:"serviceKey,omitempty"`
|
||||||
|
// Expiry - expiration time in seconds for JWTs
|
||||||
|
// +kubebuilder:default=3600
|
||||||
|
Expiry int `json:"expiry,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s JwtSpec) GetJWTSecret(ctx context.Context, client client.Client) ([]byte, error) {
|
||||||
|
var secret corev1.Secret
|
||||||
|
if err := client.Get(ctx, types.NamespacedName{Name: s.SecretRef.Name}, &secret); err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok := secret.Data[s.SecretKey]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrNoSuchSecretValue, s.SecretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s JwtSpec) SecretKeySelector() *corev1.SecretKeySelector {
|
||||||
|
return &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: *s.SecretRef,
|
||||||
|
Key: s.SecretKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s JwtSpec) JwksKeySelector() *corev1.SecretKeySelector {
|
||||||
|
return &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: *s.SecretRef,
|
||||||
|
Key: s.JwksKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s JwtSpec) SecretAsEnv(key string) corev1.EnvVar {
|
||||||
|
return corev1.EnvVar{
|
||||||
|
Name: key,
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: *s.SecretRef,
|
||||||
|
Key: s.SecretKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s JwtSpec) ExpiryAsEnv(key string) corev1.EnvVar {
|
||||||
|
return corev1.EnvVar{
|
||||||
|
Name: key,
|
||||||
|
Value: strconv.Itoa(s.Expiry),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostgrestSpec struct {
|
||||||
|
// Schemas - schema where PostgREST is looking for objects (tables, views, functions, ...)
|
||||||
|
// +kubebuilder:default={"public","graphql_public"}
|
||||||
|
Schemas []string `json:"schemas,omitempty"`
|
||||||
|
// ExtraSearchPath - Extra schemas to add to the search_path of every request.
|
||||||
|
// These schemas tables, views and functions don’t get API endpoints, they can only be referred from the database objects inside your db-schemas.
|
||||||
|
// +kubebuilder:default={"public","extensions"}
|
||||||
|
ExtraSearchPath []string `json:"extraSearchPath,omitempty"`
|
||||||
|
// AnonRole - name of the anon role
|
||||||
|
// +kubebuilder:default=anon
|
||||||
|
AnonRole string `json:"anonRole,omitempty"`
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthProviderMeta struct {
|
||||||
|
// Enabled - whether the authentication provider is enabled or not
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AuthProviderMeta) Vars(provider string) []corev1.EnvVar {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []corev1.EnvVar{{
|
||||||
|
Name: fmt.Sprintf("GOTRUE_EXTERNAL_%s_ENABLED", strings.ToUpper(provider)),
|
||||||
|
Value: strconv.FormatBool(p.Enabled),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmailAuthSmtpSpec struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port uint16 `json:"port"`
|
||||||
|
MaxFrequency *uint `json:"maxFrequency,omitempty"`
|
||||||
|
CredentialsFrom *corev1.LocalObjectReference `json:"credentialsFrom"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmailAuthProvider struct {
|
||||||
|
AuthProviderMeta `json:",inline"`
|
||||||
|
AdminEmail string `json:"adminEmail"`
|
||||||
|
SenderName *string `json:"senderName,omitempty"`
|
||||||
|
Autoconfirm *bool `json:"autoconfirmEmail,omitempty"`
|
||||||
|
SubjectsInvite string `json:"subjectsInvite,omitempty"`
|
||||||
|
SubjectsConfirmation string `json:"subjectsConfirmation,omitempty"`
|
||||||
|
SmtpSpec *EmailAuthSmtpSpec `json:"smtpSpec"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *EmailAuthProvider) Vars(apiExternalURL string) []corev1.EnvVar {
|
||||||
|
if p == nil || p.SmtpSpec == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
svcDefaults := supabase.ServiceConfig.Auth.Defaults
|
||||||
|
|
||||||
|
vars := []corev1.EnvVar{
|
||||||
|
{Name: "GOTRUE_SMTP_HOST", Value: p.SmtpSpec.Host},
|
||||||
|
{Name: "GOTRUE_SMTP_PORT", Value: strconv.FormatUint(uint64(p.SmtpSpec.Port), 10)},
|
||||||
|
{
|
||||||
|
Name: "GOTRUE_SMTP_USER",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: *p.SmtpSpec.CredentialsFrom,
|
||||||
|
Key: corev1.BasicAuthUsernameKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "GOTRUE_SMTP_PASS",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: *p.SmtpSpec.CredentialsFrom,
|
||||||
|
Key: corev1.BasicAuthPasswordKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{Name: "GOTRUE_SMTP_ADMIN_EMAIL", Value: p.AdminEmail},
|
||||||
|
{Name: "MAILER_URLPATHS_INVITE", Value: path.Join(apiExternalURL, svcDefaults.MailerUrlPathsInvite)},
|
||||||
|
{Name: "MAILER_URLPATHS_CONFIRMATION", Value: path.Join(apiExternalURL, svcDefaults.MailerUrlPathsConfirmation)},
|
||||||
|
{Name: "MAILER_URLPATHS_RECOVERY", Value: path.Join(apiExternalURL, svcDefaults.MailerUrlPathsRecovery)},
|
||||||
|
{Name: "MAILER_URLPATHS_EMAIL_CHANGE", Value: path.Join(apiExternalURL, svcDefaults.MailerUrlPathsEmailChange)},
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.SubjectsInvite != "" {
|
||||||
|
vars = append(vars, corev1.EnvVar{Name: "MAILER_SUBJECTS_INVITE", Value: p.SubjectsInvite})
|
||||||
|
}
|
||||||
|
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
|
||||||
|
type PhoneAuthProvider struct {
|
||||||
|
AuthProviderMeta `json:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PhoneAuthProvider) Vars() []corev1.EnvVar {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []corev1.EnvVar{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type OAuthProvider struct {
|
||||||
|
ClientID string `json:"clientID"`
|
||||||
|
ClientSecretRef *corev1.SecretKeySelector `json:"clientSecretRef"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OAuthProvider) Vars(provider, apiExternalURL string) []corev1.EnvVar {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := []corev1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: fmt.Sprintf("GOTRUE_EXTERNAL_%s_CLIENT_ID", strings.ToUpper(provider)),
|
||||||
|
Value: p.ClientID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: fmt.Sprintf("GOTRUE_EXTERNAL_%s_REDIRECT_URI", strings.ToUpper(provider)),
|
||||||
|
Value: path.Join(apiExternalURL, "/auth/v1/callback"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: fmt.Sprintf("GOTRUE_EXTERNAL_%s_SECRET", strings.ToUpper(provider)),
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: p.ClientSecretRef,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.URL != "" {
|
||||||
|
vars = append(vars, corev1.EnvVar{
|
||||||
|
Name: fmt.Sprintf("GOTRUE_EXTERNAL_%s_URL", strings.ToUpper(provider)),
|
||||||
|
Value: p.URL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
|
||||||
|
type AzureAuthProvider struct {
|
||||||
|
AuthProviderMeta `json:",inline"`
|
||||||
|
OAuthProvider `json:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AzureAuthProvider) Vars(apiExternalURL string) []corev1.EnvVar {
|
||||||
|
const providerName = "AZURE"
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Concat(
|
||||||
|
p.AuthProviderMeta.Vars(providerName),
|
||||||
|
p.OAuthProvider.Vars(providerName, apiExternalURL),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GithubAuthProvider struct {
|
||||||
|
AuthProviderMeta `json:",inline"`
|
||||||
|
OAuthProvider `json:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GithubAuthProvider) Vars(apiExternalURL string) []corev1.EnvVar {
|
||||||
|
const providerName = "GITHUB"
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Concat(
|
||||||
|
p.AuthProviderMeta.Vars(providerName),
|
||||||
|
p.OAuthProvider.Vars(providerName, apiExternalURL),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthProviders struct {
|
||||||
|
Email *EmailAuthProvider `json:"email,omitempty"`
|
||||||
|
Azure *AzureAuthProvider `json:"azure,omitempty"`
|
||||||
|
Github *GithubAuthProvider `json:"github,omitempty"`
|
||||||
|
Phone *PhoneAuthProvider `json:"phone,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AuthProviders) Vars(apiExternalURL string) []corev1.EnvVar {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Concat(
|
||||||
|
p.Email.Vars(apiExternalURL),
|
||||||
|
p.Azure.Vars(apiExternalURL),
|
||||||
|
p.Github.Vars(apiExternalURL),
|
||||||
|
p.Phone.Vars(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthSpec struct {
|
||||||
|
// APIExternalURL is referring to the URL where Supabase API will be available
|
||||||
|
// Typically this is the ingress of the API gateway
|
||||||
|
APIExternalURL string `json:"externalUrl"`
|
||||||
|
// SiteURL is referring to the URL of the (frontend) application
|
||||||
|
// In most Kubernetes scenarios this is the same as the APIExternalURL with a different path handler in the ingress
|
||||||
|
SiteURL string `json:"siteUrl"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
// CoreSpec defines the desired state of Core.
|
// CoreSpec defines the desired state of Core.
|
||||||
type CoreSpec struct {
|
type CoreSpec struct {
|
||||||
// Important: Run "make" to regenerate code after modifying this file
|
JWT *JwtSpec `json:"jwt,omitempty"`
|
||||||
|
|
||||||
Database Database `json:"database,omitempty"`
|
Database Database `json:"database,omitempty"`
|
||||||
|
Postgrest PostgrestSpec `json:"postgrest,omitempty"`
|
||||||
|
Auth *AuthSpec `json:"auth,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MigrationStatus map[string]int64
|
type MigrationStatus map[string]int64
|
||||||
|
@ -72,11 +392,26 @@ func (s MigrationStatus) Record(name string) {
|
||||||
s[name] = time.Now().UTC().UnixMilli()
|
s[name] = time.Now().UTC().UnixMilli()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DatabaseStatus struct {
|
||||||
|
AppliedMigrations MigrationStatus `json:"appliedMigrations,omitempty"`
|
||||||
|
Roles map[string][]byte `json:"roles,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CoreConditionType string
|
||||||
|
|
||||||
|
type CoreCondition struct {
|
||||||
|
Type CoreConditionType `json:"type"`
|
||||||
|
Status corev1.ConditionStatus `json:"status"`
|
||||||
|
LastProbeTime metav1.Time `json:"lastProbeTime,omitempty"`
|
||||||
|
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// CoreStatus defines the observed state of Core.
|
// CoreStatus defines the observed state of Core.
|
||||||
type CoreStatus struct {
|
type CoreStatus struct {
|
||||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
Database DatabaseStatus `json:"database,omitempty"`
|
||||||
// Important: Run "make" to regenerate code after modifying this file
|
Conditions []CoreCondition `json:"conditions,omitempty"`
|
||||||
AppliedMigrations MigrationStatus `json:"appliedMigrations,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
|
@ -100,6 +435,12 @@ type CoreList struct {
|
||||||
Items []Core `json:"items"`
|
Items []Core `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func (l CoreList) Iter() iter.Seq[*Core] {
|
||||||
SchemeBuilder.Register(&Core{}, &CoreList{})
|
return func(yield func(*Core) bool) {
|
||||||
|
for _, c := range l.Items {
|
||||||
|
if !yield(&c) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
96
api/v1alpha1/dashboard_types.go
Normal file
96
api/v1alpha1/dashboard_types.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StudioSpec struct {
|
||||||
|
// WorkloadTemplate - customize the studio deployment
|
||||||
|
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PGMetaSpec struct {
|
||||||
|
// WorkloadTemplate - customize the pg-meta deployment
|
||||||
|
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardDbSpec struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
// Port - Database port, typically 5432
|
||||||
|
// +kubebuilder:default=5432
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
DBName string `json:"dbName"`
|
||||||
|
// DBCredentialsRef - reference to a Secret key where the DB credentials can be retrieved from
|
||||||
|
// Credentials need to be stored in basic auth form
|
||||||
|
DBCredentialsRef *corev1.LocalObjectReference `json:"dbCredentialsRef"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DashboardDbSpec) UserRef() *corev1.SecretKeySelector {
|
||||||
|
return &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: *s.DBCredentialsRef,
|
||||||
|
Key: corev1.BasicAuthUsernameKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DashboardDbSpec) PasswordRef() *corev1.SecretKeySelector {
|
||||||
|
return &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: *s.DBCredentialsRef,
|
||||||
|
Key: corev1.BasicAuthPasswordKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DashboardSpec defines the desired state of Dashboard.
|
||||||
|
type DashboardSpec struct {
|
||||||
|
DBSpec *DashboardDbSpec `json:"db"`
|
||||||
|
// PGMeta
|
||||||
|
// +kubebuilder:default={}
|
||||||
|
PGMeta *PGMetaSpec `json:"pgMeta,omitempty"`
|
||||||
|
// Studio
|
||||||
|
// +kubebuilder:default={}
|
||||||
|
Studio *StudioSpec `json:"studio,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DashboardStatus defines the observed state of Dashboard.
|
||||||
|
type DashboardStatus struct{}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
// +kubebuilder:subresource:status
|
||||||
|
|
||||||
|
// Dashboard is the Schema for the dashboards API.
|
||||||
|
type Dashboard struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Spec DashboardSpec `json:"spec,omitempty"`
|
||||||
|
Status DashboardStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
|
||||||
|
// DashboardList contains a list of Dashboard.
|
||||||
|
type DashboardList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
Items []Dashboard `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(&Dashboard{}, &DashboardList{})
|
||||||
|
}
|
|
@ -21,15 +21,283 @@ limitations under the License.
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *APIGateway) DeepCopyInto(out *APIGateway) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
|
out.Status = in.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIGateway.
|
||||||
|
func (in *APIGateway) DeepCopy() *APIGateway {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(APIGateway)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *APIGateway) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *APIGatewayList) DeepCopyInto(out *APIGatewayList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]APIGateway, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIGatewayList.
|
||||||
|
func (in *APIGatewayList) DeepCopy() *APIGatewayList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(APIGatewayList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *APIGatewayList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *APIGatewaySpec) DeepCopyInto(out *APIGatewaySpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.Envoy != nil {
|
||||||
|
in, out := &in.Envoy, &out.Envoy
|
||||||
|
*out = new(EnvoySpec)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.JWKSSelector != nil {
|
||||||
|
in, out := &in.JWKSSelector, &out.JWKSSelector
|
||||||
|
*out = new(v1.SecretKeySelector)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIGatewaySpec.
|
||||||
|
func (in *APIGatewaySpec) DeepCopy() *APIGatewaySpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(APIGatewaySpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *APIGatewayStatus) DeepCopyInto(out *APIGatewayStatus) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIGatewayStatus.
|
||||||
|
func (in *APIGatewayStatus) DeepCopy() *APIGatewayStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(APIGatewayStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *AuthProviderMeta) DeepCopyInto(out *AuthProviderMeta) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthProviderMeta.
|
||||||
|
func (in *AuthProviderMeta) DeepCopy() *AuthProviderMeta {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(AuthProviderMeta)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *AuthProviders) DeepCopyInto(out *AuthProviders) {
|
||||||
|
*out = *in
|
||||||
|
if in.Email != nil {
|
||||||
|
in, out := &in.Email, &out.Email
|
||||||
|
*out = new(EmailAuthProvider)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.Azure != nil {
|
||||||
|
in, out := &in.Azure, &out.Azure
|
||||||
|
*out = new(AzureAuthProvider)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.Github != nil {
|
||||||
|
in, out := &in.Github, &out.Github
|
||||||
|
*out = new(GithubAuthProvider)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.Phone != nil {
|
||||||
|
in, out := &in.Phone, &out.Phone
|
||||||
|
*out = new(PhoneAuthProvider)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthProviders.
|
||||||
|
func (in *AuthProviders) DeepCopy() *AuthProviders {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(AuthProviders)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *AuthSpec) DeepCopyInto(out *AuthSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.AdditionalRedirectUrls != nil {
|
||||||
|
in, out := &in.AdditionalRedirectUrls, &out.AdditionalRedirectUrls
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.DisableSignup != nil {
|
||||||
|
in, out := &in.DisableSignup, &out.DisableSignup
|
||||||
|
*out = new(bool)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.AnonymousUsersEnabled != nil {
|
||||||
|
in, out := &in.AnonymousUsersEnabled, &out.AnonymousUsersEnabled
|
||||||
|
*out = new(bool)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.Providers != nil {
|
||||||
|
in, out := &in.Providers, &out.Providers
|
||||||
|
*out = new(AuthProviders)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.WorkloadTemplate != nil {
|
||||||
|
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
|
||||||
|
*out = new(WorkloadTemplate)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.EmailSignupDisabled != nil {
|
||||||
|
in, out := &in.EmailSignupDisabled, &out.EmailSignupDisabled
|
||||||
|
*out = new(bool)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthSpec.
|
||||||
|
func (in *AuthSpec) DeepCopy() *AuthSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(AuthSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *AzureAuthProvider) DeepCopyInto(out *AzureAuthProvider) {
|
||||||
|
*out = *in
|
||||||
|
out.AuthProviderMeta = in.AuthProviderMeta
|
||||||
|
in.OAuthProvider.DeepCopyInto(&out.OAuthProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureAuthProvider.
|
||||||
|
func (in *AzureAuthProvider) DeepCopy() *AzureAuthProvider {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(AzureAuthProvider)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ContainerTemplate) DeepCopyInto(out *ContainerTemplate) {
|
||||||
|
*out = *in
|
||||||
|
out.ImageSpec = in.ImageSpec
|
||||||
|
if in.ImagePullSecrets != nil {
|
||||||
|
in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
|
||||||
|
*out = make([]v1.LocalObjectReference, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.SecurityContext != nil {
|
||||||
|
in, out := &in.SecurityContext, &out.SecurityContext
|
||||||
|
*out = new(v1.SecurityContext)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
in.Resources.DeepCopyInto(&out.Resources)
|
||||||
|
if in.VolumeMounts != nil {
|
||||||
|
in, out := &in.VolumeMounts, &out.VolumeMounts
|
||||||
|
*out = make([]v1.VolumeMount, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if in.AdditionalEnv != nil {
|
||||||
|
in, out := &in.AdditionalEnv, &out.AdditionalEnv
|
||||||
|
*out = make([]v1.EnvVar, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerTemplate.
|
||||||
|
func (in *ContainerTemplate) DeepCopy() *ContainerTemplate {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ContainerTemplate)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ControlPlaneSpec) DeepCopyInto(out *ControlPlaneSpec) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneSpec.
|
||||||
|
func (in *ControlPlaneSpec) DeepCopy() *ControlPlaneSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ControlPlaneSpec)
|
||||||
|
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 *Core) DeepCopyInto(out *Core) {
|
func (in *Core) DeepCopyInto(out *Core) {
|
||||||
*out = *in
|
*out = *in
|
||||||
out.TypeMeta = in.TypeMeta
|
out.TypeMeta = in.TypeMeta
|
||||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
out.Spec = in.Spec
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
in.Status.DeepCopyInto(&out.Status)
|
in.Status.DeepCopyInto(&out.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +319,23 @@ func (in *Core) 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 *CoreCondition) DeepCopyInto(out *CoreCondition) {
|
||||||
|
*out = *in
|
||||||
|
in.LastProbeTime.DeepCopyInto(&out.LastProbeTime)
|
||||||
|
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoreCondition.
|
||||||
|
func (in *CoreCondition) DeepCopy() *CoreCondition {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(CoreCondition)
|
||||||
|
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 *CoreList) DeepCopyInto(out *CoreList) {
|
func (in *CoreList) DeepCopyInto(out *CoreList) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -86,7 +371,18 @@ func (in *CoreList) DeepCopyObject() runtime.Object {
|
||||||
// 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 *CoreSpec) DeepCopyInto(out *CoreSpec) {
|
func (in *CoreSpec) DeepCopyInto(out *CoreSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
out.Database = in.Database
|
if in.JWT != nil {
|
||||||
|
in, out := &in.JWT, &out.JWT
|
||||||
|
*out = new(JwtSpec)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
in.Database.DeepCopyInto(&out.Database)
|
||||||
|
in.Postgrest.DeepCopyInto(&out.Postgrest)
|
||||||
|
if in.Auth != nil {
|
||||||
|
in, out := &in.Auth, &out.Auth
|
||||||
|
*out = new(AuthSpec)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoreSpec.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoreSpec.
|
||||||
|
@ -102,11 +398,12 @@ func (in *CoreSpec) DeepCopy() *CoreSpec {
|
||||||
// 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 *CoreStatus) DeepCopyInto(out *CoreStatus) {
|
func (in *CoreStatus) DeepCopyInto(out *CoreStatus) {
|
||||||
*out = *in
|
*out = *in
|
||||||
if in.AppliedMigrations != nil {
|
in.Database.DeepCopyInto(&out.Database)
|
||||||
in, out := &in.AppliedMigrations, &out.AppliedMigrations
|
if in.Conditions != nil {
|
||||||
*out = make(map[string]uint64, len(*in))
|
in, out := &in.Conditions, &out.Conditions
|
||||||
for key, val := range *in {
|
*out = make([]CoreCondition, len(*in))
|
||||||
(*out)[key] = val
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,9 +418,144 @@ func (in *CoreStatus) DeepCopy() *CoreStatus {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *Dashboard) DeepCopyInto(out *Dashboard) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
|
out.Status = in.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dashboard.
|
||||||
|
func (in *Dashboard) DeepCopy() *Dashboard {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(Dashboard)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *Dashboard) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *DashboardDbSpec) DeepCopyInto(out *DashboardDbSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.DBCredentialsRef != nil {
|
||||||
|
in, out := &in.DBCredentialsRef, &out.DBCredentialsRef
|
||||||
|
*out = new(v1.LocalObjectReference)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardDbSpec.
|
||||||
|
func (in *DashboardDbSpec) DeepCopy() *DashboardDbSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(DashboardDbSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *DashboardList) DeepCopyInto(out *DashboardList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]Dashboard, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardList.
|
||||||
|
func (in *DashboardList) DeepCopy() *DashboardList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(DashboardList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *DashboardList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *DashboardSpec) DeepCopyInto(out *DashboardSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.DBSpec != nil {
|
||||||
|
in, out := &in.DBSpec, &out.DBSpec
|
||||||
|
*out = new(DashboardDbSpec)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.PGMeta != nil {
|
||||||
|
in, out := &in.PGMeta, &out.PGMeta
|
||||||
|
*out = new(PGMetaSpec)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.Studio != nil {
|
||||||
|
in, out := &in.Studio, &out.Studio
|
||||||
|
*out = new(StudioSpec)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSpec.
|
||||||
|
func (in *DashboardSpec) DeepCopy() *DashboardSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(DashboardSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *DashboardStatus) DeepCopyInto(out *DashboardStatus) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardStatus.
|
||||||
|
func (in *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(DashboardStatus)
|
||||||
|
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 *Database) DeepCopyInto(out *Database) {
|
func (in *Database) DeepCopyInto(out *Database) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
if in.DSN != nil {
|
||||||
|
in, out := &in.DSN, &out.DSN
|
||||||
|
*out = new(string)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.DSNSecretRef != nil {
|
||||||
|
in, out := &in.DSNSecretRef, &out.DSNSecretRef
|
||||||
|
*out = new(v1.SecretKeySelector)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
in.Roles.DeepCopyInto(&out.Roles)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Database.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Database.
|
||||||
|
@ -135,3 +567,399 @@ func (in *Database) DeepCopy() *Database {
|
||||||
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.
|
||||||
|
func (in *DatabaseRoles) DeepCopyInto(out *DatabaseRoles) {
|
||||||
|
*out = *in
|
||||||
|
in.Secrets.DeepCopyInto(&out.Secrets)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRoles.
|
||||||
|
func (in *DatabaseRoles) DeepCopy() *DatabaseRoles {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(DatabaseRoles)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *DatabaseRolesSecrets) DeepCopyInto(out *DatabaseRolesSecrets) {
|
||||||
|
*out = *in
|
||||||
|
if in.Admin != nil {
|
||||||
|
in, out := &in.Admin, &out.Admin
|
||||||
|
*out = new(v1.LocalObjectReference)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.Authenticator != nil {
|
||||||
|
in, out := &in.Authenticator, &out.Authenticator
|
||||||
|
*out = new(v1.LocalObjectReference)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.AuthAdmin != nil {
|
||||||
|
in, out := &in.AuthAdmin, &out.AuthAdmin
|
||||||
|
*out = new(v1.LocalObjectReference)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.FunctionsAdmin != nil {
|
||||||
|
in, out := &in.FunctionsAdmin, &out.FunctionsAdmin
|
||||||
|
*out = new(v1.LocalObjectReference)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.StorageAdmin != nil {
|
||||||
|
in, out := &in.StorageAdmin, &out.StorageAdmin
|
||||||
|
*out = new(v1.LocalObjectReference)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRolesSecrets.
|
||||||
|
func (in *DatabaseRolesSecrets) DeepCopy() *DatabaseRolesSecrets {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(DatabaseRolesSecrets)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if in.Roles != nil {
|
||||||
|
in, out := &in.Roles, &out.Roles
|
||||||
|
*out = make(map[string][]byte, len(*in))
|
||||||
|
for key, val := range *in {
|
||||||
|
var outVal []byte
|
||||||
|
if val == nil {
|
||||||
|
(*out)[key] = nil
|
||||||
|
} else {
|
||||||
|
inVal := (*in)[key]
|
||||||
|
in, out := &inVal, &outVal
|
||||||
|
*out = make([]byte, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
(*out)[key] = outVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseStatus.
|
||||||
|
func (in *DatabaseStatus) DeepCopy() *DatabaseStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(DatabaseStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EmailAuthProvider) DeepCopyInto(out *EmailAuthProvider) {
|
||||||
|
*out = *in
|
||||||
|
out.AuthProviderMeta = in.AuthProviderMeta
|
||||||
|
if in.SenderName != nil {
|
||||||
|
in, out := &in.SenderName, &out.SenderName
|
||||||
|
*out = new(string)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.Autoconfirm != nil {
|
||||||
|
in, out := &in.Autoconfirm, &out.Autoconfirm
|
||||||
|
*out = new(bool)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.SmtpSpec != nil {
|
||||||
|
in, out := &in.SmtpSpec, &out.SmtpSpec
|
||||||
|
*out = new(EmailAuthSmtpSpec)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmailAuthProvider.
|
||||||
|
func (in *EmailAuthProvider) DeepCopy() *EmailAuthProvider {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EmailAuthProvider)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EmailAuthSmtpSpec) DeepCopyInto(out *EmailAuthSmtpSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.MaxFrequency != nil {
|
||||||
|
in, out := &in.MaxFrequency, &out.MaxFrequency
|
||||||
|
*out = new(uint)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.CredentialsFrom != nil {
|
||||||
|
in, out := &in.CredentialsFrom, &out.CredentialsFrom
|
||||||
|
*out = new(v1.LocalObjectReference)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmailAuthSmtpSpec.
|
||||||
|
func (in *EmailAuthSmtpSpec) DeepCopy() *EmailAuthSmtpSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EmailAuthSmtpSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EnvoySpec) DeepCopyInto(out *EnvoySpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.ControlPlane != nil {
|
||||||
|
in, out := &in.ControlPlane, &out.ControlPlane
|
||||||
|
*out = new(ControlPlaneSpec)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.WorkloadTemplate != nil {
|
||||||
|
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
|
||||||
|
*out = new(WorkloadTemplate)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvoySpec.
|
||||||
|
func (in *EnvoySpec) DeepCopy() *EnvoySpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EnvoySpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *GithubAuthProvider) DeepCopyInto(out *GithubAuthProvider) {
|
||||||
|
*out = *in
|
||||||
|
out.AuthProviderMeta = in.AuthProviderMeta
|
||||||
|
in.OAuthProvider.DeepCopyInto(&out.OAuthProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GithubAuthProvider.
|
||||||
|
func (in *GithubAuthProvider) DeepCopy() *GithubAuthProvider {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(GithubAuthProvider)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ImageSpec) DeepCopyInto(out *ImageSpec) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageSpec.
|
||||||
|
func (in *ImageSpec) DeepCopy() *ImageSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ImageSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *JwtSpec) DeepCopyInto(out *JwtSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.Secret != nil {
|
||||||
|
in, out := &in.Secret, &out.Secret
|
||||||
|
*out = new(string)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.SecretRef != nil {
|
||||||
|
in, out := &in.SecretRef, &out.SecretRef
|
||||||
|
*out = new(v1.LocalObjectReference)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JwtSpec.
|
||||||
|
func (in *JwtSpec) DeepCopy() *JwtSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(JwtSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 is an autogenerated deepcopy function, copying the receiver, creating a new MigrationStatus.
|
||||||
|
func (in MigrationStatus) DeepCopy() MigrationStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(MigrationStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return *out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *OAuthProvider) DeepCopyInto(out *OAuthProvider) {
|
||||||
|
*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 OAuthProvider.
|
||||||
|
func (in *OAuthProvider) DeepCopy() *OAuthProvider {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(OAuthProvider)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PGMetaSpec.
|
||||||
|
func (in *PGMetaSpec) DeepCopy() *PGMetaSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PGMetaSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *PhoneAuthProvider) DeepCopyInto(out *PhoneAuthProvider) {
|
||||||
|
*out = *in
|
||||||
|
out.AuthProviderMeta = in.AuthProviderMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PhoneAuthProvider.
|
||||||
|
func (in *PhoneAuthProvider) DeepCopy() *PhoneAuthProvider {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PhoneAuthProvider)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *PostgrestSpec) DeepCopyInto(out *PostgrestSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.Schemas != nil {
|
||||||
|
in, out := &in.Schemas, &out.Schemas
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.ExtraSearchPath != nil {
|
||||||
|
in, out := &in.ExtraSearchPath, &out.ExtraSearchPath
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.WorkloadTemplate != nil {
|
||||||
|
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
|
||||||
|
*out = new(WorkloadTemplate)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgrestSpec.
|
||||||
|
func (in *PostgrestSpec) DeepCopy() *PostgrestSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PostgrestSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *StudioSpec) DeepCopyInto(out *StudioSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.WorkloadTemplate != nil {
|
||||||
|
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
|
||||||
|
*out = new(WorkloadTemplate)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StudioSpec.
|
||||||
|
func (in *StudioSpec) DeepCopy() *StudioSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(StudioSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *WorkloadTemplate) DeepCopyInto(out *WorkloadTemplate) {
|
||||||
|
*out = *in
|
||||||
|
if in.Replicas != nil {
|
||||||
|
in, out := &in.Replicas, &out.Replicas
|
||||||
|
*out = new(int32)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.SecurityContext != nil {
|
||||||
|
in, out := &in.SecurityContext, &out.SecurityContext
|
||||||
|
*out = new(v1.PodSecurityContext)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.AdditionalLabels != nil {
|
||||||
|
in, out := &in.AdditionalLabels, &out.AdditionalLabels
|
||||||
|
*out = make(map[string]string, len(*in))
|
||||||
|
for key, val := range *in {
|
||||||
|
(*out)[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if in.Workload != nil {
|
||||||
|
in, out := &in.Workload, &out.Workload
|
||||||
|
*out = new(ContainerTemplate)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadTemplate.
|
||||||
|
func (in *WorkloadTemplate) DeepCopy() *WorkloadTemplate {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(WorkloadTemplate)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"iter"
|
"iter"
|
||||||
"path"
|
"path"
|
||||||
|
@ -25,8 +26,18 @@ func MigrationScripts() iter.Seq2[Script, error] {
|
||||||
return readScripts(path.Join(".", "migrations"))
|
return readScripts(path.Join(".", "migrations"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RoleCreationScript(roleName string) (Script, error) {
|
||||||
|
fileName := fmt.Sprintf("%s.sql", roleName)
|
||||||
|
content, err := migrationsFS.ReadFile(path.Join("roles", fileName))
|
||||||
|
if err != nil {
|
||||||
|
return Script{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Script{fileName, string(content)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func readScripts(dir string) iter.Seq2[Script, error] {
|
func readScripts(dir string) iter.Seq2[Script, error] {
|
||||||
return iter.Seq2[Script, error](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 {
|
||||||
yield(Script{}, err)
|
yield(Script{}, err)
|
||||||
|
@ -58,5 +69,5 @@ func readScripts(dir string) iter.Seq2[Script, error] {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
5
assets/migrations/roles/supabase_functions_admin.sql
Normal file
5
assets/migrations/roles/supabase_functions_admin.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
create user supabase_functions_admin createrole noinherit;
|
||||||
|
|
||||||
|
alter user supabase_functions_admin
|
||||||
|
set
|
||||||
|
search_path = supabase_functions;
|
145
cmd/control_plane.go
Normal file
145
cmd/control_plane.go
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
clusterservice "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3"
|
||||||
|
discoverygrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
|
||||||
|
endpointservice "github.com/envoyproxy/go-control-plane/envoy/service/endpoint/v3"
|
||||||
|
listenerservice "github.com/envoyproxy/go-control-plane/envoy/service/listener/v3"
|
||||||
|
routeservice "github.com/envoyproxy/go-control-plane/envoy/service/route/v3"
|
||||||
|
runtimeservice "github.com/envoyproxy/go-control-plane/envoy/service/runtime/v3"
|
||||||
|
secretservice "github.com/envoyproxy/go-control-plane/envoy/service/secret/v3"
|
||||||
|
"github.com/envoyproxy/go-control-plane/pkg/cache/v3"
|
||||||
|
"github.com/envoyproxy/go-control-plane/pkg/server/v3"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
grpchealth "google.golang.org/grpc/health"
|
||||||
|
"google.golang.org/grpc/health/grpc_health_v1"
|
||||||
|
"google.golang.org/grpc/keepalive"
|
||||||
|
"google.golang.org/grpc/reflection"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/controlplane"
|
||||||
|
)
|
||||||
|
|
||||||
|
type controlPlane struct {
|
||||||
|
ListenAddr string `name:"listen-address" default:":18000" help:"The address the control plane binds to."`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p controlPlane) Run(ctx context.Context, cache cache.SnapshotCache) (err error) {
|
||||||
|
const (
|
||||||
|
grpcKeepaliveTime = 30 * time.Second
|
||||||
|
grpcKeepaliveTimeout = 5 * time.Second
|
||||||
|
grpcKeepaliveMinTime = 30 * time.Second
|
||||||
|
grpcMaxConcurrentStreams = 1000000
|
||||||
|
)
|
||||||
|
|
||||||
|
logger := ctrl.Log.WithName("control-plane")
|
||||||
|
|
||||||
|
clientOpts := client.Options{
|
||||||
|
Scheme: scheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Creating client")
|
||||||
|
watcherClient, err := client.NewWithWatch(ctrl.GetConfigOrDie(), clientOpts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := server.NewServer(ctx, cache, nil)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// a single connection to the management server, then it might lead to
|
||||||
|
// availability problems. Keepalive timeouts based on connection_keepalive parameter https://www.envoyproxy.io/docs/envoy/latest/configuration/overview/examples#dynamic
|
||||||
|
|
||||||
|
grpcOptions := append(make([]grpc.ServerOption, 0, 4),
|
||||||
|
grpc.MaxConcurrentStreams(grpcMaxConcurrentStreams),
|
||||||
|
grpc.KeepaliveParams(keepalive.ServerParameters{
|
||||||
|
Time: grpcKeepaliveTime,
|
||||||
|
Timeout: grpcKeepaliveTimeout,
|
||||||
|
}),
|
||||||
|
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
|
||||||
|
MinTime: grpcKeepaliveMinTime,
|
||||||
|
PermitWithoutStream: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
grpcServer := grpc.NewServer(grpcOptions...)
|
||||||
|
|
||||||
|
logger.Info("Opening listener", "addr", p.ListenAddr)
|
||||||
|
lis, err := net.Listen("tcp", p.ListenAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Preparing health endpoints")
|
||||||
|
healthService := grpchealth.NewServer()
|
||||||
|
healthService.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
|
||||||
|
|
||||||
|
reflection.Register(grpcServer)
|
||||||
|
discoverygrpc.RegisterAggregatedDiscoveryServiceServer(grpcServer, srv)
|
||||||
|
endpointservice.RegisterEndpointDiscoveryServiceServer(grpcServer, srv)
|
||||||
|
clusterservice.RegisterClusterDiscoveryServiceServer(grpcServer, srv)
|
||||||
|
routeservice.RegisterRouteDiscoveryServiceServer(grpcServer, srv)
|
||||||
|
listenerservice.RegisterListenerDiscoveryServiceServer(grpcServer, srv)
|
||||||
|
secretservice.RegisterSecretDiscoveryServiceServer(grpcServer, srv)
|
||||||
|
runtimeservice.RegisterRuntimeDiscoveryServiceServer(grpcServer, srv)
|
||||||
|
grpc_health_v1.RegisterHealthServer(grpcServer, healthService)
|
||||||
|
|
||||||
|
// discoverygrpc.AggregatedDiscoveryService_ServiceDesc.ServiceName
|
||||||
|
|
||||||
|
endpointsController := controlplane.EndpointsController{
|
||||||
|
Client: watcherClient,
|
||||||
|
Cache: cache,
|
||||||
|
}
|
||||||
|
|
||||||
|
errOut := make(chan error)
|
||||||
|
|
||||||
|
go func(errOut chan<- error) {
|
||||||
|
logger.Info("Starting gRPC server")
|
||||||
|
errOut <- grpcServer.Serve(lis)
|
||||||
|
}(errOut)
|
||||||
|
|
||||||
|
go func(errOut chan<- error) {
|
||||||
|
logger.Info("Staring endpoints controller")
|
||||||
|
errOut <- endpointsController.Run(ctx)
|
||||||
|
}(errOut)
|
||||||
|
|
||||||
|
go func(errOut chan error) {
|
||||||
|
for out := range errOut {
|
||||||
|
err = errors.Join(err, out)
|
||||||
|
}
|
||||||
|
}(errOut)
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
grpcServer.Stop()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p controlPlane) AfterApply(kongctx *kong.Context) error {
|
||||||
|
kongctx.BindTo(cache.NewSnapshotCache(false, cache.IDHash{}, nil), (*cache.SnapshotCache)(nil))
|
||||||
|
return nil
|
||||||
|
}
|
144
cmd/main.go
144
cmd/main.go
|
@ -17,26 +17,22 @@ limitations under the License.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"context"
|
||||||
"flag"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||||
// to ensure that exec-entrypoint and run can make use of them.
|
// to ensure that exec-entrypoint and run can make use of them.
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
|
||||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
|
||||||
|
|
||||||
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||||
"code.icb4dc0.de/prskr/supabase-operator/internal/controller"
|
|
||||||
// +kubebuilder:scaffold:imports
|
// +kubebuilder:scaffold:imports
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,117 +48,45 @@ func init() {
|
||||||
// +kubebuilder:scaffold:scheme
|
// +kubebuilder:scaffold:scheme
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
type app struct {
|
||||||
var metricsAddr string
|
Manager manager `cmd:"" name:"manager" help:"Run the Kubernetes operator"`
|
||||||
var enableLeaderElection bool
|
ControlPlane controlPlane `cmd:"" name:"control-plane" help:"Run the Envoy control plane"`
|
||||||
var probeAddr string
|
|
||||||
var secureMetrics bool
|
Logging struct {
|
||||||
var enableHTTP2 bool
|
Development bool `name:"development" default:"false"`
|
||||||
var tlsOpts []func(*tls.Config)
|
Level zapcore.Level `name:"level" default:"info"`
|
||||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
StacktraceLevel zapcore.Level `name:"stacktrace-level" default:"warn"`
|
||||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
} `embed:"" prefix:"logging."`
|
||||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
}
|
||||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
|
||||||
"Enable leader election for controller manager. "+
|
func (a app) AfterApply(kongctx *kong.Context) error {
|
||||||
"Enabling this will ensure there is only one active controller manager.")
|
|
||||||
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
|
||||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
|
||||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
|
||||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
|
||||||
opts := zap.Options{
|
opts := zap.Options{
|
||||||
Development: true,
|
Development: a.Logging.Development,
|
||||||
}
|
Level: a.Logging.Level,
|
||||||
opts.BindFlags(flag.CommandLine)
|
StacktraceLevel: a.Logging.StacktraceLevel,
|
||||||
flag.Parse()
|
TimeEncoder: zapcore.ISO8601TimeEncoder,
|
||||||
|
|
||||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
|
||||||
|
|
||||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
|
||||||
// due to its vulnerabilities. More specifically, disabling http/2 will
|
|
||||||
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
|
||||||
// Rapid Reset CVEs. For more information see:
|
|
||||||
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
|
||||||
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
|
||||||
disableHTTP2 := func(c *tls.Config) {
|
|
||||||
setupLog.Info("disabling http/2")
|
|
||||||
c.NextProtos = []string{"http/1.1"}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !enableHTTP2 {
|
logger := zap.New(zap.UseFlagOptions(&opts))
|
||||||
tlsOpts = append(tlsOpts, disableHTTP2)
|
ctrl.SetLogger(logger)
|
||||||
}
|
kongctx.Bind(logger)
|
||||||
|
|
||||||
webhookServer := webhook.NewServer(webhook.Options{
|
logger.Info("Completed logger setup")
|
||||||
TLSOpts: tlsOpts,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
return nil
|
||||||
// More info:
|
}
|
||||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
|
|
||||||
// - https://book.kubebuilder.io/reference/metrics.html
|
|
||||||
metricsServerOptions := metricsserver.Options{
|
|
||||||
BindAddress: metricsAddr,
|
|
||||||
SecureServing: secureMetrics,
|
|
||||||
TLSOpts: tlsOpts,
|
|
||||||
}
|
|
||||||
|
|
||||||
if secureMetrics {
|
func main() {
|
||||||
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
var app app
|
||||||
// These configurations ensure that only authorized users and service accounts
|
|
||||||
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
|
||||||
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
|
||||||
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
|
||||||
|
|
||||||
// TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
|
kongCtx := kong.Parse(
|
||||||
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
&app,
|
||||||
// this setup is not recommended for production.
|
kong.Name("supabase-operator"),
|
||||||
}
|
kong.BindTo(ctrl.SetupSignalHandler(), (*context.Context)(nil)),
|
||||||
|
)
|
||||||
|
|
||||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
if err := kongCtx.Run(); err != nil {
|
||||||
Scheme: scheme,
|
setupLog.Error(err, "failed to run app")
|
||||||
Metrics: metricsServerOptions,
|
|
||||||
WebhookServer: webhookServer,
|
|
||||||
HealthProbeBindAddress: probeAddr,
|
|
||||||
LeaderElection: enableLeaderElection,
|
|
||||||
LeaderElectionID: "05f9463f.k8s.icb4dc0.de",
|
|
||||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
|
||||||
// when the Manager ends. This requires the binary to immediately end when the
|
|
||||||
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
|
||||||
// speeds up voluntary leader transitions as the new leader don't have to wait
|
|
||||||
// LeaseDuration time first.
|
|
||||||
//
|
|
||||||
// In the default scaffold provided, the program ends immediately after
|
|
||||||
// the manager stops, so would be fine to enable this option. However,
|
|
||||||
// if you are doing or is intended to do any operation such as perform cleanups
|
|
||||||
// after the manager stops then its usage might be unsafe.
|
|
||||||
// LeaderElectionReleaseOnCancel: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
setupLog.Error(err, "unable to start manager")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = (&controller.CoreReconciler{
|
|
||||||
Client: mgr.GetClient(),
|
|
||||||
Scheme: mgr.GetScheme(),
|
|
||||||
}).SetupWithManager(mgr); err != nil {
|
|
||||||
setupLog.Error(err, "unable to create controller", "controller", "Core")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
// +kubebuilder:scaffold:builder
|
|
||||||
|
|
||||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
|
||||||
setupLog.Error(err, "unable to set up health check")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
|
||||||
setupLog.Error(err, "unable to set up ready check")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
setupLog.Info("starting manager")
|
|
||||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
|
||||||
setupLog.Error(err, "problem running manager")
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
181
cmd/manager.go
Normal file
181
cmd/manager.go
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||||
|
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/controller"
|
||||||
|
webhooksupabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/internal/webhook/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:lll // flag declaration needs to be in tags
|
||||||
|
type manager struct {
|
||||||
|
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."`
|
||||||
|
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."`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m manager) Run(ctx context.Context) error {
|
||||||
|
var tlsOpts []func(*tls.Config)
|
||||||
|
|
||||||
|
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||||
|
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||||
|
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||||
|
// Rapid Reset CVEs. For more information see:
|
||||||
|
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||||
|
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||||
|
disableHTTP2 := func(c *tls.Config) {
|
||||||
|
setupLog.Info("disabling http/2")
|
||||||
|
c.NextProtos = []string{"http/1.1"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.EnableHTTP2 {
|
||||||
|
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookConfig := webhooksupabasev1alpha1.WebhookConfig{
|
||||||
|
CurrentNamespace: m.Namespace,
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookServer := webhook.NewServer(webhook.Options{
|
||||||
|
TLSOpts: tlsOpts,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||||
|
// More info:
|
||||||
|
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
|
||||||
|
// - https://book.kubebuilder.io/reference/metrics.html
|
||||||
|
metricsServerOptions := metricsserver.Options{
|
||||||
|
BindAddress: m.MetricsAddr,
|
||||||
|
SecureServing: m.SecureMetrics,
|
||||||
|
TLSOpts: tlsOpts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.SecureMetrics {
|
||||||
|
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||||
|
// These configurations ensure that only authorized users and service accounts
|
||||||
|
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||||
|
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||||
|
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||||
|
Scheme: scheme,
|
||||||
|
Metrics: metricsServerOptions,
|
||||||
|
WebhookServer: webhookServer,
|
||||||
|
HealthProbeBindAddress: m.ProbeAddr,
|
||||||
|
LeaderElection: m.EnableLeaderElection,
|
||||||
|
LeaderElectionID: "05f9463f.k8s.icb4dc0.de",
|
||||||
|
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||||
|
// when the Manager ends. This requires the binary to immediately end when the
|
||||||
|
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||||
|
// speeds up voluntary leader transitions as the new leader don't have to wait
|
||||||
|
// LeaseDuration time first.
|
||||||
|
//
|
||||||
|
// In the default scaffold provided, the program ends immediately after
|
||||||
|
// the manager stops, so would be fine to enable this option. However,
|
||||||
|
// if you are doing or is intended to do any operation such as perform cleanups
|
||||||
|
// after the manager stops then its usage might be unsafe.
|
||||||
|
// LeaderElectionReleaseOnCancel: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to start manager: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = (&controller.CoreDbReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
return fmt.Errorf("unable to create controller Core DB: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = (&controller.CoreJwtReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
return fmt.Errorf("unable to create controller Core JWT: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = (&controller.CorePostgrestReconiler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
return fmt.Errorf("unable to create controller Core Postgrest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = (&controller.CoreAuthReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
return fmt.Errorf("unable to create controller Core Auth: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = (&controller.DashboardPGMetaReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
return fmt.Errorf("unable to create controller Dashboard PG-Meta: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:goconst
|
||||||
|
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
|
||||||
|
if err = webhooksupabasev1alpha1.SetupCoreWebhookWithManager(mgr); err != nil {
|
||||||
|
return fmt.Errorf("unable to create webhook: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = (&controller.APIGatewayReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
}).SetupWithManager(ctx, mgr); err != nil {
|
||||||
|
return fmt.Errorf("unable to create controller APIGateway: %w", err)
|
||||||
|
}
|
||||||
|
// nolint:goconst
|
||||||
|
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
|
||||||
|
if err = webhooksupabasev1alpha1.SetupAPIGatewayWebhookWithManager(mgr, webhookConfig); err != nil {
|
||||||
|
setupLog.Error(err, "unable to create webhook", "webhook", "APIGateway")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// +kubebuilder:scaffold:builder
|
||||||
|
|
||||||
|
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); 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")
|
||||||
|
if err := mgr.Start(ctx); err != nil {
|
||||||
|
return fmt.Errorf("problem running manager: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
35
config/certmanager/certificate.yaml
Normal file
35
config/certmanager/certificate.yaml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# The following manifests contain a self-signed issuer CR and a certificate CR.
|
||||||
|
# More document can be found at https://docs.cert-manager.io
|
||||||
|
# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes.
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Issuer
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: selfsigned-issuer
|
||||||
|
namespace: supabase-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: serving-cert # this name should match the one appeared in kustomizeconfig.yaml
|
||||||
|
namespace: supabase-system
|
||||||
|
spec:
|
||||||
|
# SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
|
||||||
|
dnsNames:
|
||||||
|
- SERVICE_NAME.SERVICE_NAMESPACE.svc
|
||||||
|
- SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local
|
||||||
|
issuerRef:
|
||||||
|
kind: Issuer
|
||||||
|
name: selfsigned-issuer
|
||||||
|
secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize
|
5
config/certmanager/kustomization.yaml
Normal file
5
config/certmanager/kustomization.yaml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
resources:
|
||||||
|
- certificate.yaml
|
||||||
|
|
||||||
|
configurations:
|
||||||
|
- kustomizeconfig.yaml
|
8
config/certmanager/kustomizeconfig.yaml
Normal file
8
config/certmanager/kustomizeconfig.yaml
Normal 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
|
84
config/control-plane/control-plane.yaml
Normal file
84
config/control-plane/control-plane.yaml
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: control-plane
|
||||||
|
namespace: supabase-system
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: control-plane
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: control-plane
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
kubectl.kubernetes.io/default-container: control-plane
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: control-plane
|
||||||
|
spec:
|
||||||
|
# TODO(user): Uncomment the following code to configure the nodeAffinity expression
|
||||||
|
# according to the platforms which are supported by your solution.
|
||||||
|
# It is considered best practice to support multiple architectures. You can
|
||||||
|
# build your manager image using the makefile target docker-buildx.
|
||||||
|
# affinity:
|
||||||
|
# nodeAffinity:
|
||||||
|
# requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
# nodeSelectorTerms:
|
||||||
|
# - matchExpressions:
|
||||||
|
# - key: kubernetes.io/arch
|
||||||
|
# operator: In
|
||||||
|
# values:
|
||||||
|
# - amd64
|
||||||
|
# - arm64
|
||||||
|
# - ppc64le
|
||||||
|
# - s390x
|
||||||
|
# - key: kubernetes.io/os
|
||||||
|
# operator: In
|
||||||
|
# values:
|
||||||
|
# - linux
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
# TODO(user): For common cases that do not require escalating privileges
|
||||||
|
# it is recommended to ensure that all your Pods/Containers are restrictive.
|
||||||
|
# More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
|
||||||
|
# Please uncomment the following code if your project does NOT have to work on old Kubernetes
|
||||||
|
# versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ).
|
||||||
|
# seccompProfile:
|
||||||
|
# type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- args:
|
||||||
|
- control-plane
|
||||||
|
image: supabase-operator:latest
|
||||||
|
name: control-plane
|
||||||
|
ports:
|
||||||
|
- containerPort: 18000
|
||||||
|
name: grpc
|
||||||
|
protocol: TCP
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- "ALL"
|
||||||
|
livenessProbe:
|
||||||
|
grpc:
|
||||||
|
port: 18000
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 20
|
||||||
|
readinessProbe:
|
||||||
|
grpc:
|
||||||
|
port: 18000
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
# TODO(user): Configure the resources accordingly based on the project requirements.
|
||||||
|
# More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 128Mi
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 64Mi
|
||||||
|
serviceAccountName: control-plane
|
||||||
|
terminationGracePeriodSeconds: 10
|
6
config/control-plane/kustomization.yaml
Normal file
6
config/control-plane/kustomization.yaml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- control-plane.yaml
|
||||||
|
- service.yaml
|
17
config/control-plane/service.yaml
Normal file
17
config/control-plane/service.yaml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: control-plane
|
||||||
|
namespace: supabase-system
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: control-plane
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: grpc
|
||||||
|
port: 18000
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 18000
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: control-plane
|
790
config/crd/bases/supabase.k8s.icb4dc0.de_apigateways.yaml
Normal file
790
config/crd/bases/supabase.k8s.icb4dc0.de_apigateways.yaml
Normal file
|
@ -0,0 +1,790 @@
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.16.5
|
||||||
|
name: apigateways.supabase.k8s.icb4dc0.de
|
||||||
|
spec:
|
||||||
|
group: supabase.k8s.icb4dc0.de
|
||||||
|
names:
|
||||||
|
kind: APIGateway
|
||||||
|
listKind: APIGatewayList
|
||||||
|
plural: apigateways
|
||||||
|
singular: apigateway
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: APIGateway is the Schema for the apigateways API.
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: APIGatewaySpec defines the desired state of APIGateway.
|
||||||
|
properties:
|
||||||
|
envoy:
|
||||||
|
description: Envoy - configure the envoy instance and most importantly
|
||||||
|
the control-plane
|
||||||
|
properties:
|
||||||
|
controlPlane:
|
||||||
|
description: ControlPlane - configure the control plane where
|
||||||
|
Envoy will retrieve its configuration from
|
||||||
|
properties:
|
||||||
|
host:
|
||||||
|
description: Host is the hostname of the envoy control plane
|
||||||
|
endpoint
|
||||||
|
type: string
|
||||||
|
port:
|
||||||
|
default: 18000
|
||||||
|
description: Port is the port number of the envoy control
|
||||||
|
plane endpoint - typically this is 18000
|
||||||
|
maximum: 65535
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- host
|
||||||
|
- port
|
||||||
|
type: object
|
||||||
|
workloadTemplate:
|
||||||
|
description: WorkloadTemplate - customize the Envoy deployment
|
||||||
|
properties:
|
||||||
|
additionalLabels:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
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
|
||||||
|
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:
|
||||||
|
additionalEnv:
|
||||||
|
items:
|
||||||
|
description: EnvVar represents an environment variable
|
||||||
|
present in a Container.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: Name of the environment variable. Must
|
||||||
|
be a C_IDENTIFIER.
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
description: |-
|
||||||
|
Variable references $(VAR_NAME) are expanded
|
||||||
|
using the previously defined environment variables in the container and
|
||||||
|
any service environment variables. If a variable cannot be resolved,
|
||||||
|
the reference in the input string will be unchanged. Double $$ are reduced
|
||||||
|
to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e.
|
||||||
|
"$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)".
|
||||||
|
Escaped references will never be expanded, regardless of whether the variable
|
||||||
|
exists or not.
|
||||||
|
Defaults to "".
|
||||||
|
type: string
|
||||||
|
valueFrom:
|
||||||
|
description: Source for the environment variable's
|
||||||
|
value. Cannot be used if value is not empty.
|
||||||
|
properties:
|
||||||
|
configMapKeyRef:
|
||||||
|
description: Selects a key of a ConfigMap.
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
description: The key to select.
|
||||||
|
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 ConfigMap
|
||||||
|
or its key must be defined
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- key
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
|
fieldRef:
|
||||||
|
description: |-
|
||||||
|
Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`,
|
||||||
|
spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: Version of the schema the FieldPath
|
||||||
|
is written in terms of, defaults to "v1".
|
||||||
|
type: string
|
||||||
|
fieldPath:
|
||||||
|
description: Path of the field to select
|
||||||
|
in the specified API version.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- fieldPath
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
|
resourceFieldRef:
|
||||||
|
description: |-
|
||||||
|
Selects a resource of the container: only resources limits and requests
|
||||||
|
(limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.
|
||||||
|
properties:
|
||||||
|
containerName:
|
||||||
|
description: 'Container name: required for
|
||||||
|
volumes, optional for env vars'
|
||||||
|
type: string
|
||||||
|
divisor:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
description: Specifies the output format
|
||||||
|
of the exposed resources, defaults to
|
||||||
|
"1"
|
||||||
|
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
resource:
|
||||||
|
description: 'Required: resource to select'
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- resource
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
|
secretKeyRef:
|
||||||
|
description: Selects a key of a secret in the
|
||||||
|
pod's namespace
|
||||||
|
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
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
image:
|
||||||
|
type: string
|
||||||
|
imagePullSecrets:
|
||||||
|
items:
|
||||||
|
description: |-
|
||||||
|
LocalObjectReference contains enough information to let you locate the
|
||||||
|
referenced object inside the same namespace.
|
||||||
|
properties:
|
||||||
|
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
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
|
type: array
|
||||||
|
pullPolicy:
|
||||||
|
description: PullPolicy describes a policy for if/when
|
||||||
|
to pull a container image
|
||||||
|
type: string
|
||||||
|
resources:
|
||||||
|
description: ResourceRequirements describes the compute
|
||||||
|
resource requirements.
|
||||||
|
properties:
|
||||||
|
claims:
|
||||||
|
description: |-
|
||||||
|
Claims lists the names of resources, defined in spec.resourceClaims,
|
||||||
|
that are used by this container.
|
||||||
|
|
||||||
|
This is an alpha field and requires enabling the
|
||||||
|
DynamicResourceAllocation feature gate.
|
||||||
|
|
||||||
|
This field is immutable. It can only be set for containers.
|
||||||
|
items:
|
||||||
|
description: ResourceClaim references one entry
|
||||||
|
in PodSpec.ResourceClaims.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: |-
|
||||||
|
Name must match the name of one entry in pod.spec.resourceClaims of
|
||||||
|
the Pod where this field is used. It makes that resource available
|
||||||
|
inside a container.
|
||||||
|
type: string
|
||||||
|
request:
|
||||||
|
description: |-
|
||||||
|
Request is the name chosen for a request in the referenced claim.
|
||||||
|
If empty, everything from the claim is made available, otherwise
|
||||||
|
only the result of this request.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- name
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
limits:
|
||||||
|
additionalProperties:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
description: |-
|
||||||
|
Limits describes the maximum amount of compute resources allowed.
|
||||||
|
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||||
|
type: object
|
||||||
|
requests:
|
||||||
|
additionalProperties:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
description: |-
|
||||||
|
Requests describes the minimum amount of compute resources required.
|
||||||
|
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
|
||||||
|
otherwise to an implementation-defined value. Requests cannot exceed Limits.
|
||||||
|
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
securityContext:
|
||||||
|
description: SecurityContext -
|
||||||
|
properties:
|
||||||
|
allowPrivilegeEscalation:
|
||||||
|
description: |-
|
||||||
|
AllowPrivilegeEscalation controls whether a process can gain more
|
||||||
|
privileges than its parent process. This bool directly controls if
|
||||||
|
the no_new_privs flag will be set on the container process.
|
||||||
|
AllowPrivilegeEscalation is true always when the container is:
|
||||||
|
1) run as Privileged
|
||||||
|
2) has CAP_SYS_ADMIN
|
||||||
|
Note that this field cannot be set when spec.os.name is windows.
|
||||||
|
type: boolean
|
||||||
|
appArmorProfile:
|
||||||
|
description: |-
|
||||||
|
appArmorProfile is the AppArmor options to use by this container. If set, this profile
|
||||||
|
overrides the pod's appArmorProfile.
|
||||||
|
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
|
||||||
|
capabilities:
|
||||||
|
description: |-
|
||||||
|
The capabilities to add/drop when running containers.
|
||||||
|
Defaults to the default set of capabilities granted by the container runtime.
|
||||||
|
Note that this field cannot be set when spec.os.name is windows.
|
||||||
|
properties:
|
||||||
|
add:
|
||||||
|
description: Added capabilities
|
||||||
|
items:
|
||||||
|
description: Capability represent POSIX capabilities
|
||||||
|
type
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
drop:
|
||||||
|
description: Removed capabilities
|
||||||
|
items:
|
||||||
|
description: Capability represent POSIX capabilities
|
||||||
|
type
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
type: object
|
||||||
|
privileged:
|
||||||
|
description: |-
|
||||||
|
Run container in privileged mode.
|
||||||
|
Processes in privileged containers are essentially equivalent to root on the host.
|
||||||
|
Defaults to false.
|
||||||
|
Note that this field cannot be set when spec.os.name is windows.
|
||||||
|
type: boolean
|
||||||
|
procMount:
|
||||||
|
description: |-
|
||||||
|
procMount denotes the type of proc mount to use for the containers.
|
||||||
|
The default value is Default which uses the container runtime defaults for
|
||||||
|
readonly paths and masked paths.
|
||||||
|
This requires the ProcMountType feature flag to be enabled.
|
||||||
|
Note that this field cannot be set when spec.os.name is windows.
|
||||||
|
type: string
|
||||||
|
readOnlyRootFilesystem:
|
||||||
|
description: |-
|
||||||
|
Whether this container has a read-only root filesystem.
|
||||||
|
Default is false.
|
||||||
|
Note that this field cannot be set when spec.os.name is windows.
|
||||||
|
type: boolean
|
||||||
|
runAsGroup:
|
||||||
|
description: |-
|
||||||
|
The GID to run the entrypoint of the container process.
|
||||||
|
Uses runtime default if unset.
|
||||||
|
May also be set in PodSecurityContext. 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 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 PodSecurityContext. 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 PodSecurityContext. 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 windows.
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
seLinuxOptions:
|
||||||
|
description: |-
|
||||||
|
The SELinux context to be applied to the container.
|
||||||
|
If unspecified, the container runtime will allocate a random SELinux context for each
|
||||||
|
container. May also be set in PodSecurityContext. 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 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 this container. If seccomp options are
|
||||||
|
provided at both the pod & container level, the container options
|
||||||
|
override the pod options.
|
||||||
|
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
|
||||||
|
windowsOptions:
|
||||||
|
description: |-
|
||||||
|
The Windows specific settings applied to all containers.
|
||||||
|
If unspecified, the options from the PodSecurityContext 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
|
||||||
|
volumeMounts:
|
||||||
|
items:
|
||||||
|
description: VolumeMount describes a mounting of a Volume
|
||||||
|
within a container.
|
||||||
|
properties:
|
||||||
|
mountPath:
|
||||||
|
description: |-
|
||||||
|
Path within the container at which the volume should be mounted. Must
|
||||||
|
not contain ':'.
|
||||||
|
type: string
|
||||||
|
mountPropagation:
|
||||||
|
description: |-
|
||||||
|
mountPropagation determines how mounts are propagated from the host
|
||||||
|
to container and the other way around.
|
||||||
|
When not set, MountPropagationNone is used.
|
||||||
|
This field is beta in 1.10.
|
||||||
|
When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified
|
||||||
|
(which defaults to None).
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: This must match the Name of a Volume.
|
||||||
|
type: string
|
||||||
|
readOnly:
|
||||||
|
description: |-
|
||||||
|
Mounted read-only if true, read-write otherwise (false or unspecified).
|
||||||
|
Defaults to false.
|
||||||
|
type: boolean
|
||||||
|
recursiveReadOnly:
|
||||||
|
description: |-
|
||||||
|
RecursiveReadOnly specifies whether read-only mounts should be handled
|
||||||
|
recursively.
|
||||||
|
|
||||||
|
If ReadOnly is false, this field has no meaning and must be unspecified.
|
||||||
|
|
||||||
|
If ReadOnly is true, and this field is set to Disabled, the mount is not made
|
||||||
|
recursively read-only. If this field is set to IfPossible, the mount is made
|
||||||
|
recursively read-only, if it is supported by the container runtime. If this
|
||||||
|
field is set to Enabled, the mount is made recursively read-only if it is
|
||||||
|
supported by the container runtime, otherwise the pod will not be started and
|
||||||
|
an error will be generated to indicate the reason.
|
||||||
|
|
||||||
|
If this field is set to IfPossible or Enabled, MountPropagation must be set to
|
||||||
|
None (or be unspecified, which defaults to None).
|
||||||
|
|
||||||
|
If this field is not specified, it is treated as an equivalent of Disabled.
|
||||||
|
type: string
|
||||||
|
subPath:
|
||||||
|
description: |-
|
||||||
|
Path within the volume from which the container's volume should be mounted.
|
||||||
|
Defaults to "" (volume's root).
|
||||||
|
type: string
|
||||||
|
subPathExpr:
|
||||||
|
description: |-
|
||||||
|
Expanded path within the volume from which the container's volume should be mounted.
|
||||||
|
Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment.
|
||||||
|
Defaults to "" (volume's root).
|
||||||
|
SubPathExpr and SubPath are mutually exclusive.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- mountPath
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- securityContext
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- controlPlane
|
||||||
|
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
|
||||||
|
required:
|
||||||
|
- envoy
|
||||||
|
- jwks
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: APIGatewayStatus defines the observed state of APIGateway.
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
File diff suppressed because it is too large
Load diff
1469
config/crd/bases/supabase.k8s.icb4dc0.de_dashboards.yaml
Normal file
1469
config/crd/bases/supabase.k8s.icb4dc0.de_dashboards.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -2,8 +2,10 @@
|
||||||
# since it depends on service name and namespace that are out of this kustomize package.
|
# since it depends on service name and namespace that are out of this kustomize package.
|
||||||
# It should be run by config/default
|
# It should be run by config/default
|
||||||
resources:
|
resources:
|
||||||
- bases/supabase.k8s.icb4dc0.de_cores.yaml
|
- bases/supabase.k8s.icb4dc0.de_cores.yaml
|
||||||
# +kubebuilder:scaffold:crdkustomizeresource
|
- bases/supabase.k8s.icb4dc0.de_apigateways.yaml
|
||||||
|
- bases/supabase.k8s.icb4dc0.de_dashboards.yaml
|
||||||
|
# +kubebuilder:scaffold:crdkustomizeresource
|
||||||
|
|
||||||
patches:
|
patches:
|
||||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
|
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
|
||||||
|
@ -16,5 +18,5 @@ patches:
|
||||||
|
|
||||||
# [WEBHOOK] To enable webhook, uncomment the following section
|
# [WEBHOOK] To enable webhook, uncomment the following section
|
||||||
# the following config is for teaching kustomize how to do kustomization for CRDs.
|
# the following config is for teaching kustomize how to do kustomization for CRDs.
|
||||||
#configurations:
|
configurations:
|
||||||
#- kustomizeconfig.yaml
|
- kustomizeconfig.yaml
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
# Adds namespace to all resources.
|
# Adds namespace to all resources.
|
||||||
namespace: supabase-operator-system
|
namespace: supabase-system
|
||||||
|
|
||||||
# Value of this field is prepended to the
|
# Value of this field is prepended to the
|
||||||
# names of all resources, e.g. a deployment named
|
# names of all resources, e.g. a deployment named
|
||||||
# "wordpress" becomes "alices-wordpress".
|
# "wordpress" becomes "alices-wordpress".
|
||||||
# Note that it should also match with the prefix (text before '-') of the namespace
|
# Note that it should also match with the prefix (text before '-') of the namespace
|
||||||
# field above.
|
# field above.
|
||||||
namePrefix: supabase-operator-
|
namePrefix: supabase-
|
||||||
|
|
||||||
# Labels to add to all resources and selectors.
|
# Labels to add to all resources and selectors.
|
||||||
#labels:
|
#labels:
|
||||||
|
@ -15,18 +15,16 @@ namePrefix: supabase-operator-
|
||||||
# someName: someValue
|
# someName: someValue
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
- ../crd
|
- ../crd
|
||||||
- ../rbac
|
- ../rbac
|
||||||
- ../manager
|
- ../manager
|
||||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
- ../control-plane
|
||||||
# crd/kustomization.yaml
|
- ../webhook
|
||||||
#- ../webhook
|
- ../certmanager
|
||||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
|
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
|
||||||
#- ../certmanager
|
#- ../prometheus
|
||||||
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
|
# [METRICS] Expose the controller manager metrics service.
|
||||||
#- ../prometheus
|
- metrics_service.yaml
|
||||||
# [METRICS] Expose the controller manager metrics service.
|
|
||||||
- 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.
|
||||||
# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will
|
# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will
|
||||||
|
@ -35,116 +33,116 @@ resources:
|
||||||
|
|
||||||
# Uncomment the patches line if you enable Metrics, and/or are using webhooks and cert-manager
|
# Uncomment the patches line if you enable Metrics, and/or are using webhooks and cert-manager
|
||||||
patches:
|
patches:
|
||||||
# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
|
# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
|
||||||
# More info: https://book.kubebuilder.io/reference/metrics
|
# More info: https://book.kubebuilder.io/reference/metrics
|
||||||
- path: manager_metrics_patch.yaml
|
- path: manager_metrics_patch.yaml
|
||||||
target:
|
target:
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
|
name: controller-manager
|
||||||
|
|
||||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
||||||
# 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.
|
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
|
||||||
# Uncomment the following replacements to add the cert-manager CA injection annotations
|
# 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: # Uncomment the following block if you have any webhook
|
||||||
# kind: Service
|
kind: Service
|
||||||
# version: v1
|
version: v1
|
||||||
# name: webhook-service
|
name: webhook-service
|
||||||
# fieldPath: .metadata.name # Name of the service
|
fieldPath: .metadata.name # Name of the service
|
||||||
# targets:
|
targets:
|
||||||
# - select:
|
- select:
|
||||||
# kind: Certificate
|
kind: Certificate
|
||||||
# group: cert-manager.io
|
group: cert-manager.io
|
||||||
# version: v1
|
version: v1
|
||||||
# fieldPaths:
|
fieldPaths:
|
||||||
# - .spec.dnsNames.0
|
- .spec.dnsNames.0
|
||||||
# - .spec.dnsNames.1
|
- .spec.dnsNames.1
|
||||||
# options:
|
options:
|
||||||
# delimiter: '.'
|
delimiter: "."
|
||||||
# index: 0
|
index: 0
|
||||||
# create: true
|
create: true
|
||||||
# - source:
|
- source:
|
||||||
# kind: Service
|
kind: Service
|
||||||
# version: v1
|
version: v1
|
||||||
# name: webhook-service
|
name: webhook-service
|
||||||
# fieldPath: .metadata.namespace # Namespace of the service
|
fieldPath: .metadata.namespace # Namespace of the service
|
||||||
# targets:
|
targets:
|
||||||
# - select:
|
- select:
|
||||||
# kind: Certificate
|
kind: Certificate
|
||||||
# group: cert-manager.io
|
group: cert-manager.io
|
||||||
# version: v1
|
version: v1
|
||||||
# fieldPaths:
|
fieldPaths:
|
||||||
# - .spec.dnsNames.0
|
- .spec.dnsNames.0
|
||||||
# - .spec.dnsNames.1
|
- .spec.dnsNames.1
|
||||||
# options:
|
options:
|
||||||
# delimiter: '.'
|
delimiter: "."
|
||||||
# index: 1
|
index: 1
|
||||||
# create: true
|
create: true
|
||||||
#
|
|
||||||
# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
|
- source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
|
||||||
# kind: Certificate
|
kind: Certificate
|
||||||
# group: cert-manager.io
|
group: cert-manager.io
|
||||||
# version: v1
|
version: v1
|
||||||
# name: serving-cert # This name should match the one in certificate.yaml
|
name: serving-cert # This name should match the one in certificate.yaml
|
||||||
# fieldPath: .metadata.namespace # Namespace of the certificate CR
|
fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||||
# targets:
|
targets:
|
||||||
# - select:
|
- select:
|
||||||
# kind: ValidatingWebhookConfiguration
|
kind: ValidatingWebhookConfiguration
|
||||||
# fieldPaths:
|
fieldPaths:
|
||||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
- .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||||
# options:
|
options:
|
||||||
# delimiter: '/'
|
delimiter: "/"
|
||||||
# index: 0
|
index: 0
|
||||||
# create: true
|
create: true
|
||||||
# - source:
|
- source:
|
||||||
# kind: Certificate
|
kind: Certificate
|
||||||
# group: cert-manager.io
|
group: cert-manager.io
|
||||||
# version: v1
|
version: v1
|
||||||
# name: serving-cert # This name should match the one in certificate.yaml
|
name: serving-cert # This name should match the one in certificate.yaml
|
||||||
# fieldPath: .metadata.name
|
fieldPath: .metadata.name
|
||||||
# targets:
|
targets:
|
||||||
# - select:
|
- select:
|
||||||
# kind: ValidatingWebhookConfiguration
|
kind: ValidatingWebhookConfiguration
|
||||||
# fieldPaths:
|
fieldPaths:
|
||||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
- .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||||
# options:
|
options:
|
||||||
# delimiter: '/'
|
delimiter: "/"
|
||||||
# index: 1
|
index: 1
|
||||||
# create: true
|
create: true
|
||||||
#
|
|
||||||
# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
|
- source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
|
||||||
# kind: Certificate
|
kind: Certificate
|
||||||
# group: cert-manager.io
|
group: cert-manager.io
|
||||||
# version: v1
|
version: v1
|
||||||
# name: serving-cert # This name should match the one in certificate.yaml
|
name: serving-cert # This name should match the one in certificate.yaml
|
||||||
# fieldPath: .metadata.namespace # Namespace of the certificate CR
|
fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||||
# targets:
|
targets:
|
||||||
# - select:
|
- select:
|
||||||
# kind: MutatingWebhookConfiguration
|
kind: MutatingWebhookConfiguration
|
||||||
# fieldPaths:
|
fieldPaths:
|
||||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
- .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||||
# options:
|
options:
|
||||||
# delimiter: '/'
|
delimiter: "/"
|
||||||
# index: 0
|
index: 0
|
||||||
# create: true
|
create: true
|
||||||
# - source:
|
- source:
|
||||||
# kind: Certificate
|
kind: Certificate
|
||||||
# group: cert-manager.io
|
group: cert-manager.io
|
||||||
# version: v1
|
version: v1
|
||||||
# name: serving-cert # This name should match the one in certificate.yaml
|
name: serving-cert # This name should match the one in certificate.yaml
|
||||||
# fieldPath: .metadata.name
|
fieldPath: .metadata.name
|
||||||
# targets:
|
targets:
|
||||||
# - select:
|
- select:
|
||||||
# kind: MutatingWebhookConfiguration
|
kind: MutatingWebhookConfiguration
|
||||||
# fieldPaths:
|
fieldPaths:
|
||||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
- .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||||
# options:
|
options:
|
||||||
# delimiter: '/'
|
delimiter: "/"
|
||||||
# index: 1
|
index: 1
|
||||||
# create: true
|
create: true
|
||||||
#
|
|
||||||
# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
|
# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
|
||||||
# kind: Certificate
|
# kind: Certificate
|
||||||
# group: cert-manager.io
|
# group: cert-manager.io
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
|
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
|
||||||
- op: add
|
- op: add
|
||||||
path: /spec/template/spec/containers/0/args/0
|
path: /spec/template/spec/containers/0/args/1
|
||||||
value: --metrics-bind-address=:8443
|
value: --metrics-bind-address=:8443
|
||||||
|
|
26
config/default/manager_webhook_patch.yaml
Normal file
26
config/default/manager_webhook_patch.yaml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: controller-manager
|
||||||
|
namespace: supabase-system
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: manager
|
||||||
|
ports:
|
||||||
|
- containerPort: 9443
|
||||||
|
name: webhook-server
|
||||||
|
protocol: TCP
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /tmp/k8s-webhook-server/serving-certs
|
||||||
|
name: cert
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: cert
|
||||||
|
secret:
|
||||||
|
defaultMode: 420
|
||||||
|
secretName: webhook-server-cert
|
|
@ -6,7 +6,7 @@ metadata:
|
||||||
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: controller-manager-metrics-service
|
name: controller-manager-metrics-service
|
||||||
namespace: system
|
namespace: supabase-system
|
||||||
spec:
|
spec:
|
||||||
ports:
|
ports:
|
||||||
- name: https
|
- name: https
|
||||||
|
|
10
config/dev/kustomization.yaml
Normal file
10
config/dev/kustomization.yaml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- ../default
|
||||||
|
|
||||||
|
patches:
|
||||||
|
- path: manager_dev_settings.yaml
|
||||||
|
target:
|
||||||
|
kind: Deployment
|
12
config/dev/manager_dev_settings.yaml
Normal file
12
config/dev/manager_dev_settings.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
- op: add
|
||||||
|
path: /spec/template/spec/containers/0/args/1
|
||||||
|
value: --logging.development=true
|
||||||
|
- op: replace
|
||||||
|
path: /spec/template/spec/containers/0/resources
|
||||||
|
value:
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 768Mi
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 384Mi
|
|
@ -5,13 +5,13 @@ metadata:
|
||||||
control-plane: controller-manager
|
control-plane: controller-manager
|
||||||
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: system
|
name: supabase-system
|
||||||
---
|
---
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: controller-manager
|
name: controller-manager
|
||||||
namespace: system
|
namespace: supabase-system
|
||||||
labels:
|
labels:
|
||||||
control-plane: controller-manager
|
control-plane: controller-manager
|
||||||
app.kubernetes.io/name: supabase-operator
|
app.kubernetes.io/name: supabase-operator
|
||||||
|
@ -50,21 +50,20 @@ spec:
|
||||||
# - linux
|
# - 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:
|
||||||
- command:
|
- args:
|
||||||
- /manager
|
- manager
|
||||||
args:
|
|
||||||
- --leader-elect
|
- --leader-elect
|
||||||
- --health-probe-bind-address=:8081
|
- --health-probe-bind-address=:8081
|
||||||
image: controller:latest
|
image: supabase-operator:latest
|
||||||
name: manager
|
name: manager
|
||||||
|
env:
|
||||||
|
- name: CONTROLLER_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
capabilities:
|
capabilities:
|
||||||
|
|
|
@ -8,7 +8,7 @@ metadata:
|
||||||
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: allow-metrics-traffic
|
name: allow-metrics-traffic
|
||||||
namespace: system
|
namespace: supabase-system
|
||||||
spec:
|
spec:
|
||||||
podSelector:
|
podSelector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
|
|
26
config/network-policy/allow-webhook-traffic.yaml
Normal file
26
config/network-policy/allow-webhook-traffic.yaml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# This NetworkPolicy allows ingress traffic to your webhook server running
|
||||||
|
# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks
|
||||||
|
# will only work when applied in namespaces labeled with 'webhook: enabled'
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: allow-webhook-traffic
|
||||||
|
namespace: supabase-system
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
control-plane: controller-manager
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
# This allows ingress traffic from any namespace with the label webhook: enabled
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
webhook: enabled # Only from namespaces with this label
|
||||||
|
ports:
|
||||||
|
- port: 443
|
||||||
|
protocol: TCP
|
|
@ -1,2 +1,3 @@
|
||||||
resources:
|
resources:
|
||||||
|
- allow-webhook-traffic.yaml
|
||||||
- allow-metrics-traffic.yaml
|
- allow-metrics-traffic.yaml
|
||||||
|
|
|
@ -7,7 +7,7 @@ metadata:
|
||||||
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: controller-manager-metrics-monitor
|
name: controller-manager-metrics-monitor
|
||||||
namespace: system
|
namespace: supabase-system
|
||||||
spec:
|
spec:
|
||||||
endpoints:
|
endpoints:
|
||||||
- path: /metrics
|
- path: /metrics
|
||||||
|
|
27
config/rbac/apigateway_editor_role.yaml
Normal file
27
config/rbac/apigateway_editor_role.yaml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# permissions for end users to edit apigateways.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: apigateway-editor-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
resources:
|
||||||
|
- apigateways
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
resources:
|
||||||
|
- apigateways/status
|
||||||
|
verbs:
|
||||||
|
- get
|
23
config/rbac/apigateway_viewer_role.yaml
Normal file
23
config/rbac/apigateway_viewer_role.yaml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# permissions for end users to view apigateways.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: apigateway-viewer-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
resources:
|
||||||
|
- apigateways
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
resources:
|
||||||
|
- apigateways/status
|
||||||
|
verbs:
|
||||||
|
- get
|
14
config/rbac/control-plane-role.yaml
Normal file
14
config/rbac/control-plane-role.yaml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: control-plane-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- discovery.k8s.io
|
||||||
|
resources:
|
||||||
|
- endpointslices
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
15
config/rbac/control-plane-role_binding.yaml
Normal file
15
config/rbac/control-plane-role_binding.yaml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: control-plane-rolebinding
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: control-plane
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: control-plane-role
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: control-plane
|
||||||
|
namespace: supabase-system
|
8
config/rbac/control-plane-service_account.yaml
Normal file
8
config/rbac/control-plane-service_account.yaml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: control-plane
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: control-plane
|
||||||
|
namespace: supabase-system
|
27
config/rbac/dashboard_editor_role.yaml
Normal file
27
config/rbac/dashboard_editor_role.yaml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# permissions for end users to edit dashboards.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: dashboard-editor-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
resources:
|
||||||
|
- dashboards
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
resources:
|
||||||
|
- dashboards/status
|
||||||
|
verbs:
|
||||||
|
- get
|
23
config/rbac/dashboard_viewer_role.yaml
Normal file
23
config/rbac/dashboard_viewer_role.yaml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# permissions for end users to view dashboards.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: dashboard-viewer-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
resources:
|
||||||
|
- dashboards
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
resources:
|
||||||
|
- dashboards/status
|
||||||
|
verbs:
|
||||||
|
- get
|
|
@ -1,27 +1,38 @@
|
||||||
resources:
|
resources:
|
||||||
# All RBAC will be applied under this service account in
|
# All RBAC will be applied under this service account in
|
||||||
# the deployment namespace. You may comment out this resource
|
# the deployment namespace. You may comment out this resource
|
||||||
# if your manager will use a service account that exists at
|
# if your manager will use a service account that exists at
|
||||||
# runtime. Be sure to update RoleBinding and ClusterRoleBinding
|
# runtime. Be sure to update RoleBinding and ClusterRoleBinding
|
||||||
# subjects if changing service account names.
|
# subjects if changing service account names.
|
||||||
- service_account.yaml
|
- service_account.yaml
|
||||||
- role.yaml
|
- role.yaml
|
||||||
- role_binding.yaml
|
- role_binding.yaml
|
||||||
- leader_election_role.yaml
|
- leader_election_role.yaml
|
||||||
- leader_election_role_binding.yaml
|
- leader_election_role_binding.yaml
|
||||||
# The following RBAC configurations are used to protect
|
# RBAC role for the control plane
|
||||||
# the metrics endpoint with authn/authz. These configurations
|
- control-plane-service_account.yaml
|
||||||
# ensure that only authorized users and service accounts
|
- control-plane-role.yaml
|
||||||
# can access the metrics endpoint. Comment the following
|
- control-plane-role_binding.yaml
|
||||||
# permissions if you want to disable this protection.
|
# The following RBAC configurations are used to protect
|
||||||
# More info: https://book.kubebuilder.io/reference/metrics.html
|
# the metrics endpoint with authn/authz. These configurations
|
||||||
- metrics_auth_role.yaml
|
# ensure that only authorized users and service accounts
|
||||||
- metrics_auth_role_binding.yaml
|
# can access the metrics endpoint. Comment the following
|
||||||
- metrics_reader_role.yaml
|
# permissions if you want to disable this protection.
|
||||||
# For each CRD, "Editor" and "Viewer" roles are scaffolded by
|
# More info: https://book.kubebuilder.io/reference/metrics.html
|
||||||
# default, aiding admins in cluster management. Those roles are
|
- metrics_auth_role.yaml
|
||||||
# not used by the Project itself. You can comment the following lines
|
- metrics_auth_role_binding.yaml
|
||||||
# if you do not want those helpers be installed with your Project.
|
- metrics_reader_role.yaml
|
||||||
- core_editor_role.yaml
|
# For each CRD, "Editor" and "Viewer" roles are scaffolded by
|
||||||
- core_viewer_role.yaml
|
# default, aiding admins in cluster management. Those roles are
|
||||||
|
# not used by the Project itself. You can comment the following lines
|
||||||
|
# if you do not want those helpers be installed with your Project.
|
||||||
|
- apigateway_editor_role.yaml
|
||||||
|
- apigateway_viewer_role.yaml
|
||||||
|
- core_editor_role.yaml
|
||||||
|
- core_viewer_role.yaml
|
||||||
|
# For each CRD, "Editor" and "Viewer" roles are scaffolded by
|
||||||
|
# default, aiding admins in cluster management. Those roles are
|
||||||
|
# not used by the Project itself. You can comment the following lines
|
||||||
|
# if you do not want those helpers be installed with your Project.
|
||||||
|
- dashboard_editor_role.yaml
|
||||||
|
- dashboard_viewer_role.yaml
|
||||||
|
|
|
@ -10,6 +10,6 @@ roleRef:
|
||||||
kind: Role
|
kind: Role
|
||||||
name: leader-election-role
|
name: leader-election-role
|
||||||
subjects:
|
subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: controller-manager
|
name: controller-manager
|
||||||
namespace: system
|
namespace: supabase-system
|
||||||
|
|
|
@ -7,6 +7,6 @@ roleRef:
|
||||||
kind: ClusterRole
|
kind: ClusterRole
|
||||||
name: metrics-auth-role
|
name: metrics-auth-role
|
||||||
subjects:
|
subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: controller-manager
|
name: controller-manager
|
||||||
namespace: system
|
namespace: supabase-system
|
||||||
|
|
|
@ -5,9 +5,29 @@ metadata:
|
||||||
name: manager-role
|
name: manager-role
|
||||||
rules:
|
rules:
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- supabase.k8s.icb4dc0.de
|
- ""
|
||||||
resources:
|
resources:
|
||||||
- cores
|
- configmaps
|
||||||
|
- secrets
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- events
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- deployments
|
||||||
verbs:
|
verbs:
|
||||||
- create
|
- create
|
||||||
- delete
|
- delete
|
||||||
|
@ -19,13 +39,31 @@ rules:
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- supabase.k8s.icb4dc0.de
|
- supabase.k8s.icb4dc0.de
|
||||||
resources:
|
resources:
|
||||||
|
- apigateways
|
||||||
|
- cores
|
||||||
|
- dashboards
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
resources:
|
||||||
|
- apigateways/finalizers
|
||||||
- cores/finalizers
|
- cores/finalizers
|
||||||
|
- dashboards/finalizers
|
||||||
verbs:
|
verbs:
|
||||||
- update
|
- update
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- supabase.k8s.icb4dc0.de
|
- supabase.k8s.icb4dc0.de
|
||||||
resources:
|
resources:
|
||||||
|
- apigateways/status
|
||||||
- cores/status
|
- cores/status
|
||||||
|
- dashboards/status
|
||||||
verbs:
|
verbs:
|
||||||
- get
|
- get
|
||||||
- patch
|
- patch
|
||||||
|
|
|
@ -10,6 +10,6 @@ roleRef:
|
||||||
kind: ClusterRole
|
kind: ClusterRole
|
||||||
name: manager-role
|
name: manager-role
|
||||||
subjects:
|
subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: controller-manager
|
name: controller-manager
|
||||||
namespace: system
|
namespace: supabase-system
|
||||||
|
|
|
@ -5,4 +5,4 @@ metadata:
|
||||||
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: controller-manager
|
name: controller-manager
|
||||||
namespace: system
|
namespace: supabase-system
|
||||||
|
|
|
@ -40,7 +40,7 @@ metadata:
|
||||||
name: cluster-example
|
name: cluster-example
|
||||||
spec:
|
spec:
|
||||||
instances: 1
|
instances: 1
|
||||||
imageName: ghcr.io/supabase/postgres:15.6.1.145
|
imageName: ghcr.io/supabase/postgres:15.8.1.021
|
||||||
postgresUID: 105
|
postgresUID: 105
|
||||||
postgresGID: 106
|
postgresGID: 106
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
## Append samples of your project ##
|
## Append samples of your project ##
|
||||||
|
|
||||||
|
namespace: supabase-demo
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
- supabase_v1alpha1_core.yaml
|
- namespace.yaml
|
||||||
# +kubebuilder:scaffold:manifestskustomizesamples
|
- cnpg-cluster.yaml
|
||||||
|
- supabase_v1alpha1_core.yaml
|
||||||
|
- supabase_v1alpha1_apigateway.yaml
|
||||||
|
- supabase_v1alpha1_dashboard.yaml
|
||||||
|
# +kubebuilder:scaffold:manifestskustomizesamples
|
||||||
|
|
4
config/samples/namespace.yaml
Normal file
4
config/samples/namespace.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: supabase-demo
|
12
config/samples/supabase_v1alpha1_apigateway.yaml
Normal file
12
config/samples/supabase_v1alpha1_apigateway.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
|
||||||
|
kind: APIGateway
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: core-sample
|
||||||
|
spec:
|
||||||
|
envoy:
|
||||||
|
controlPlane:
|
||||||
|
host: supabase-control-plane.supabase-system.svc
|
||||||
|
port: 18000
|
|
@ -1,3 +1,10 @@
|
||||||
|
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:
|
||||||
|
@ -8,5 +15,11 @@ metadata:
|
||||||
spec:
|
spec:
|
||||||
database:
|
database:
|
||||||
dsnFrom:
|
dsnFrom:
|
||||||
name: example-cluster-credentials
|
name: supabase-demo-credentials
|
||||||
key: url
|
key: url
|
||||||
|
auth:
|
||||||
|
externalUrl: http://localhost:8000/
|
||||||
|
siteUrl: http://localhost:3000/
|
||||||
|
disableSignup: true
|
||||||
|
enableEmailAutoconfirm: true
|
||||||
|
providers: {}
|
||||||
|
|
13
config/samples/supabase_v1alpha1_dashboard.yaml
Normal file
13
config/samples/supabase_v1alpha1_dashboard.yaml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
|
||||||
|
kind: Dashboard
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: core-sample
|
||||||
|
spec:
|
||||||
|
db:
|
||||||
|
host: cluster-example-rw.supabase-demo
|
||||||
|
dbName: app
|
||||||
|
dbCredentialsRef:
|
||||||
|
name: db-roles-creds-supabase-admin
|
6
config/webhook/kustomization.yaml
Normal file
6
config/webhook/kustomization.yaml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
resources:
|
||||||
|
- manifests.yaml
|
||||||
|
- service.yaml
|
||||||
|
|
||||||
|
configurations:
|
||||||
|
- kustomizeconfig.yaml
|
22
config/webhook/kustomizeconfig.yaml
Normal file
22
config/webhook/kustomizeconfig.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# the following config is for teaching kustomize where to look at when substituting nameReference.
|
||||||
|
# It requires kustomize v2.1.0 or newer to work properly.
|
||||||
|
nameReference:
|
||||||
|
- kind: Service
|
||||||
|
version: v1
|
||||||
|
fieldSpecs:
|
||||||
|
- kind: MutatingWebhookConfiguration
|
||||||
|
group: admissionregistration.k8s.io
|
||||||
|
path: webhooks/clientConfig/service/name
|
||||||
|
- kind: ValidatingWebhookConfiguration
|
||||||
|
group: admissionregistration.k8s.io
|
||||||
|
path: webhooks/clientConfig/service/name
|
||||||
|
|
||||||
|
namespace:
|
||||||
|
- kind: MutatingWebhookConfiguration
|
||||||
|
group: admissionregistration.k8s.io
|
||||||
|
path: webhooks/clientConfig/service/namespace
|
||||||
|
create: true
|
||||||
|
- kind: ValidatingWebhookConfiguration
|
||||||
|
group: admissionregistration.k8s.io
|
||||||
|
path: webhooks/clientConfig/service/namespace
|
||||||
|
create: true
|
92
config/webhook/manifests.yaml
Normal file
92
config/webhook/manifests.yaml
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
---
|
||||||
|
apiVersion: admissionregistration.k8s.io/v1
|
||||||
|
kind: MutatingWebhookConfiguration
|
||||||
|
metadata:
|
||||||
|
name: mutating-webhook-configuration
|
||||||
|
webhooks:
|
||||||
|
- admissionReviewVersions:
|
||||||
|
- v1
|
||||||
|
clientConfig:
|
||||||
|
service:
|
||||||
|
name: webhook-service
|
||||||
|
namespace: system
|
||||||
|
path: /mutate-supabase-k8s-icb4dc0-de-v1alpha1-apigateway
|
||||||
|
failurePolicy: Fail
|
||||||
|
name: mapigateway-v1alpha1.kb.io
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
apiVersions:
|
||||||
|
- v1alpha1
|
||||||
|
operations:
|
||||||
|
- CREATE
|
||||||
|
- UPDATE
|
||||||
|
resources:
|
||||||
|
- apigateways
|
||||||
|
sideEffects: None
|
||||||
|
- admissionReviewVersions:
|
||||||
|
- v1
|
||||||
|
clientConfig:
|
||||||
|
service:
|
||||||
|
name: webhook-service
|
||||||
|
namespace: system
|
||||||
|
path: /mutate-supabase-k8s-icb4dc0-de-v1alpha1-core
|
||||||
|
failurePolicy: Fail
|
||||||
|
name: mcore-v1alpha1.kb.io
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
apiVersions:
|
||||||
|
- v1alpha1
|
||||||
|
operations:
|
||||||
|
- CREATE
|
||||||
|
- UPDATE
|
||||||
|
resources:
|
||||||
|
- cores
|
||||||
|
sideEffects: None
|
||||||
|
---
|
||||||
|
apiVersion: admissionregistration.k8s.io/v1
|
||||||
|
kind: ValidatingWebhookConfiguration
|
||||||
|
metadata:
|
||||||
|
name: validating-webhook-configuration
|
||||||
|
webhooks:
|
||||||
|
- admissionReviewVersions:
|
||||||
|
- v1
|
||||||
|
clientConfig:
|
||||||
|
service:
|
||||||
|
name: webhook-service
|
||||||
|
namespace: system
|
||||||
|
path: /validate-supabase-k8s-icb4dc0-de-v1alpha1-apigateway
|
||||||
|
failurePolicy: Fail
|
||||||
|
name: vapigateway-v1alpha1.kb.io
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
apiVersions:
|
||||||
|
- v1alpha1
|
||||||
|
operations:
|
||||||
|
- CREATE
|
||||||
|
- UPDATE
|
||||||
|
resources:
|
||||||
|
- apigateways
|
||||||
|
sideEffects: None
|
||||||
|
- admissionReviewVersions:
|
||||||
|
- v1
|
||||||
|
clientConfig:
|
||||||
|
service:
|
||||||
|
name: webhook-service
|
||||||
|
namespace: system
|
||||||
|
path: /validate-supabase-k8s-icb4dc0-de-v1alpha1-core
|
||||||
|
failurePolicy: Fail
|
||||||
|
name: vcore-v1alpha1.kb.io
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- supabase.k8s.icb4dc0.de
|
||||||
|
apiVersions:
|
||||||
|
- v1alpha1
|
||||||
|
operations:
|
||||||
|
- CREATE
|
||||||
|
- UPDATE
|
||||||
|
resources:
|
||||||
|
- cores
|
||||||
|
sideEffects: None
|
15
config/webhook/service.yaml
Normal file
15
config/webhook/service.yaml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: webhook-service
|
||||||
|
namespace: supabase-system
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 443
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 9443
|
||||||
|
selector:
|
||||||
|
control-plane: controller-manager
|
9
crd-docs.yaml
Normal file
9
crd-docs.yaml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
processor:
|
||||||
|
ignoreTypes: []
|
||||||
|
ignoreFields:
|
||||||
|
- "status$"
|
||||||
|
- "TypeMeta$"
|
||||||
|
|
||||||
|
render:
|
||||||
|
kubernetesVersion: "1.30"
|
||||||
|
knownTypes: []
|
5
dev/Dockerfile
Normal file
5
dev/Dockerfile
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
FROM alpine:3.21
|
||||||
|
WORKDIR /app
|
||||||
|
ADD --chown=65532:65532 ./out ./bin
|
||||||
|
USER 65532:65532
|
||||||
|
ENTRYPOINT [ "/app/bin/supabase-operator" ]
|
9
dev/cluster.yaml
Normal file
9
dev/cluster.yaml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
apiVersion: ctlptl.dev/v1alpha1
|
||||||
|
kind: Registry
|
||||||
|
name: ctlptl-registry
|
||||||
|
port: 5005
|
||||||
|
---
|
||||||
|
apiVersion: ctlptl.dev/v1alpha1
|
||||||
|
kind: Cluster
|
||||||
|
product: kind
|
||||||
|
registry: ctlptl-registry
|
4
dev/prepare-dev-cluster.sh
Executable file
4
dev/prepare-dev-cluster.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml
|
||||||
|
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.24/releases/cnpg-1.24.1.yaml
|
560
docs/api/supabase.k8s.icb4dc0.de.md
Normal file
560
docs/api/supabase.k8s.icb4dc0.de.md
Normal file
|
@ -0,0 +1,560 @@
|
||||||
|
# API Reference
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
- [supabase.k8s.icb4dc0.de/v1alpha1](#supabasek8sicb4dc0dev1alpha1)
|
||||||
|
|
||||||
|
|
||||||
|
## supabase.k8s.icb4dc0.de/v1alpha1
|
||||||
|
|
||||||
|
Package v1alpha1 contains API Schema definitions for the supabase v1alpha1 API group.
|
||||||
|
|
||||||
|
### Resource Types
|
||||||
|
- [APIGateway](#apigateway)
|
||||||
|
- [APIGatewayList](#apigatewaylist)
|
||||||
|
- [Core](#core)
|
||||||
|
- [CoreList](#corelist)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### APIGateway
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
APIGateway is the Schema for the apigateways API.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [APIGatewayList](#apigatewaylist)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `apiVersion` _string_ | `supabase.k8s.icb4dc0.de/v1alpha1` | | |
|
||||||
|
| `kind` _string_ | `APIGateway` | | |
|
||||||
|
| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
|
||||||
|
| `spec` _[APIGatewaySpec](#apigatewayspec)_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### APIGatewayList
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
APIGatewayList contains a list of APIGateway.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `apiVersion` _string_ | `supabase.k8s.icb4dc0.de/v1alpha1` | | |
|
||||||
|
| `kind` _string_ | `APIGatewayList` | | |
|
||||||
|
| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
|
||||||
|
| `items` _[APIGateway](#apigateway) array_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### APIGatewaySpec
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
APIGatewaySpec defines the desired state of APIGateway.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [APIGateway](#apigateway)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `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 | | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### AuthProviderMeta
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [AzureAuthProvider](#azureauthprovider)
|
||||||
|
- [EmailAuthProvider](#emailauthprovider)
|
||||||
|
- [GithubAuthProvider](#githubauthprovider)
|
||||||
|
- [PhoneAuthProvider](#phoneauthprovider)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `enabled` _boolean_ | Enabled - whether the authentication provider is enabled or not | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### AuthProviders
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [AuthSpec](#authspec)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `email` _[EmailAuthProvider](#emailauthprovider)_ | | | |
|
||||||
|
| `azure` _[AzureAuthProvider](#azureauthprovider)_ | | | |
|
||||||
|
| `github` _[GithubAuthProvider](#githubauthprovider)_ | | | |
|
||||||
|
| `phone` _[PhoneAuthProvider](#phoneauthprovider)_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### AuthSpec
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [CoreSpec](#corespec)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `externalUrl` _string_ | APIExternalURL is referring to the URL where Supabase API will be available<br />Typically this is the ingress of the API gateway | | |
|
||||||
|
| `siteUrl` _string_ | SiteURL is referring to the URL of the (frontend) application<br />In most Kubernetes scenarios this is the same as the APIExternalURL with a different path handler in the ingress | | |
|
||||||
|
| `additionalRedirectUrls` _string array_ | | | |
|
||||||
|
| `disableSignup` _boolean_ | | | |
|
||||||
|
| `anonymousUsersEnabled` _boolean_ | | | |
|
||||||
|
| `providers` _[AuthProviders](#authproviders)_ | | | |
|
||||||
|
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | | | |
|
||||||
|
| `emailSignupDisabled` _boolean_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### AzureAuthProvider
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [AuthProviders](#authproviders)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `enabled` _boolean_ | Enabled - whether the authentication provider is enabled or not | | |
|
||||||
|
| `clientID` _string_ | | | |
|
||||||
|
| `clientSecretRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretkeyselector-v1-core)_ | | | |
|
||||||
|
| `url` _string_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### ContainerTemplate
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [WorkloadTemplate](#workloadtemplate)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `image` _string_ | | | |
|
||||||
|
| `pullPolicy` _[PullPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#pullpolicy-v1-core)_ | | | |
|
||||||
|
| `imagePullSecrets` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core) array_ | | | |
|
||||||
|
| `securityContext` _[SecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#securitycontext-v1-core)_ | | | |
|
||||||
|
| `resources` _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core)_ | | | |
|
||||||
|
| `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | | | |
|
||||||
|
| `additionalEnv` _[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#envvar-v1-core) array_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### ControlPlaneSpec
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [EnvoySpec](#envoyspec)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `host` _string_ | Host is the hostname of the envoy control plane endpoint | | |
|
||||||
|
| `port` _integer_ | Port is the port number of the envoy control plane endpoint - typically this is 18000 | 18000 | Maximum: 65535 <br /> |
|
||||||
|
|
||||||
|
|
||||||
|
#### Core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Core is the Schema for the cores API.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [CoreList](#corelist)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `apiVersion` _string_ | `supabase.k8s.icb4dc0.de/v1alpha1` | | |
|
||||||
|
| `kind` _string_ | `Core` | | |
|
||||||
|
| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
|
||||||
|
| `spec` _[CoreSpec](#corespec)_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### CoreCondition
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [CoreStatus](#corestatus)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `type` _[CoreConditionType](#coreconditiontype)_ | | | |
|
||||||
|
| `lastProbeTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_ | | | |
|
||||||
|
| `lastTransitionTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_ | | | |
|
||||||
|
| `reason` _string_ | | | |
|
||||||
|
| `message` _string_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### CoreConditionType
|
||||||
|
|
||||||
|
_Underlying type:_ _string_
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [CoreCondition](#corecondition)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### CoreList
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CoreList contains a list of Core.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `apiVersion` _string_ | `supabase.k8s.icb4dc0.de/v1alpha1` | | |
|
||||||
|
| `kind` _string_ | `CoreList` | | |
|
||||||
|
| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
|
||||||
|
| `items` _[Core](#core) array_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### CoreSpec
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CoreSpec defines the desired state of Core.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [Core](#core)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `jwt` _[JwtSpec](#jwtspec)_ | | | |
|
||||||
|
| `database` _[Database](#database)_ | | | |
|
||||||
|
| `postgrest` _[PostgrestSpec](#postgrestspec)_ | | | |
|
||||||
|
| `auth` _[AuthSpec](#authspec)_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### Database
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [CoreSpec](#corespec)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `dsn` _string_ | | | |
|
||||||
|
| `dsnFrom` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretkeyselector-v1-core)_ | | | |
|
||||||
|
| `roles` _[DatabaseRoles](#databaseroles)_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### DatabaseRoles
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [Database](#database)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `selfManaged` _boolean_ | SelfManaged - whether the database roles are managed externally<br />when enabled the operator does not attempt to create secrets, generate passwords or whatsoever for all database roles<br />i.e. all secrets need to be provided or the instance won't work | | |
|
||||||
|
| `secrets` _[DatabaseRolesSecrets](#databaserolessecrets)_ | Secrets - typed 'map' of secrets for each database role that Supabase needs | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### DatabaseRolesSecrets
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [DatabaseRoles](#databaseroles)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `authenticator` _[SecretReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretreference-v1-core)_ | | | |
|
||||||
|
| `supabaseAuthAdmin` _[SecretReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretreference-v1-core)_ | | | |
|
||||||
|
| `supabaseFunctionsAdmin` _[SecretReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretreference-v1-core)_ | | | |
|
||||||
|
| `supabaseStorageAdmin` _[SecretReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretreference-v1-core)_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### DatabaseStatus
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [CoreStatus](#corestatus)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `appliedMigrations` _[MigrationStatus](#migrationstatus)_ | | | |
|
||||||
|
| `roles` _object (keys:string, values:integer array)_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### EmailAuthProvider
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [AuthProviders](#authproviders)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `enabled` _boolean_ | Enabled - whether the authentication provider is enabled or not | | |
|
||||||
|
| `adminEmail` _string_ | | | |
|
||||||
|
| `senderName` _string_ | | | |
|
||||||
|
| `autoconfirmEmail` _boolean_ | | | |
|
||||||
|
| `subjectsInvite` _string_ | | | |
|
||||||
|
| `subjectsConfirmation` _string_ | | | |
|
||||||
|
| `smtpSpec` _[EmailAuthSmtpSpec](#emailauthsmtpspec)_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### EmailAuthSmtpSpec
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [EmailAuthProvider](#emailauthprovider)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `host` _string_ | | | |
|
||||||
|
| `port` _integer_ | | | |
|
||||||
|
| `maxFrequency` _integer_ | | | |
|
||||||
|
| `credentialsFrom` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### EnvoySpec
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [APIGatewaySpec](#apigatewayspec)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `controlPlane` _[ControlPlaneSpec](#controlplanespec)_ | ControlPlane - configure the control plane where Envoy will retrieve its configuration from | | |
|
||||||
|
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the Envoy deployment | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### GithubAuthProvider
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [AuthProviders](#authproviders)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `enabled` _boolean_ | Enabled - whether the authentication provider is enabled or not | | |
|
||||||
|
| `clientID` _string_ | | | |
|
||||||
|
| `clientSecretRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretkeyselector-v1-core)_ | | | |
|
||||||
|
| `url` _string_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### ImageSpec
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [ContainerTemplate](#containertemplate)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `image` _string_ | | | |
|
||||||
|
| `pullPolicy` _[PullPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#pullpolicy-v1-core)_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### JwtSpec
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [CoreSpec](#corespec)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `secret` _string_ | Secret - JWT HMAC secret in plain text<br />This is WRITE-ONLY and will be copied to the SecretRef by the defaulter | | |
|
||||||
|
| `secretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | SecretRef - object reference to the Secret where JWT values are stored | | |
|
||||||
|
| `secretKey` _string_ | SecretKey - key in secret where to read the JWT HMAC secret from | secret | |
|
||||||
|
| `jwksKey` _string_ | JwksKey - key in secret where to read the JWKS from | jwks.json | |
|
||||||
|
| `anonKey` _string_ | AnonKey - key in secret where to read the anon JWT from | anon_key | |
|
||||||
|
| `serviceKey` _string_ | ServiceKey - key in secret where to read the service JWT from | service_key | |
|
||||||
|
| `expiry` _integer_ | Expiry - expiration time in seconds for JWTs | 3600 | |
|
||||||
|
|
||||||
|
|
||||||
|
#### MigrationStatus
|
||||||
|
|
||||||
|
_Underlying type:_ _object_
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [DatabaseStatus](#databasestatus)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### OAuthProvider
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [AzureAuthProvider](#azureauthprovider)
|
||||||
|
- [GithubAuthProvider](#githubauthprovider)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `clientID` _string_ | | | |
|
||||||
|
| `clientSecretRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretkeyselector-v1-core)_ | | | |
|
||||||
|
| `url` _string_ | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### PhoneAuthProvider
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [AuthProviders](#authproviders)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `enabled` _boolean_ | Enabled - whether the authentication provider is enabled or not | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### PostgrestSpec
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [CoreSpec](#corespec)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `schemas` _string array_ | Schemas - schema where PostgREST is looking for objects (tables, views, functions, ...) | [public graphql_public] | UniqueItems: true <br /> |
|
||||||
|
| `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] | UniqueItems: true <br /> |
|
||||||
|
| `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 | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### WorkloadTemplate
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_Appears in:_
|
||||||
|
- [AuthSpec](#authspec)
|
||||||
|
- [EnvoySpec](#envoyspec)
|
||||||
|
- [PostgrestSpec](#postgrestspec)
|
||||||
|
|
||||||
|
| Field | Description | Default | Validation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `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)_ | | | |
|
||||||
|
|
||||||
|
|
0
docs/getting_started.md
Normal file
0
docs/getting_started.md
Normal file
17
docs/index.md
Normal file
17
docs/index.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Welcome to MkDocs
|
||||||
|
|
||||||
|
For full documentation visit [mkdocs.org](https://www.mkdocs.org).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
* `mkdocs new [dir-name]` - Create a new project.
|
||||||
|
* `mkdocs serve` - Start the live-reloading docs server.
|
||||||
|
* `mkdocs build` - Build the documentation site.
|
||||||
|
* `mkdocs -h` - Print help message and exit.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
mkdocs.yml # The configuration file.
|
||||||
|
docs/
|
||||||
|
index.md # The documentation homepage.
|
||||||
|
... # Other markdown pages, images and other files.
|
18
docs/overrides/main.html
Normal file
18
docs/overrides/main.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "base.html" %} {% block extrahead %}
|
||||||
|
<!-- Add scripts that need to run before here -->
|
||||||
|
{{ super() }}
|
||||||
|
<!-- Add scripts that need to run afterwards here -->
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
data-domain="docs.supabase-operator.icb4dc0.de"
|
||||||
|
src="https://plausible.icb4dc0.de/js/script.js"
|
||||||
|
></script>
|
||||||
|
<script>
|
||||||
|
window.plausible =
|
||||||
|
window.plausible ||
|
||||||
|
function () {
|
||||||
|
(window.plausible.q = window.plausible.q || []).push(arguments);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
34
go.mod
34
go.mod
|
@ -3,11 +3,16 @@ module code.icb4dc0.de/prskr/supabase-operator
|
||||||
go 1.23.4
|
go 1.23.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alecthomas/kong v1.6.0
|
||||||
|
github.com/envoyproxy/go-control-plane v0.13.1
|
||||||
github.com/jackc/pgx/v5 v5.7.1
|
github.com/jackc/pgx/v5 v5.7.1
|
||||||
|
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.19.0
|
github.com/onsi/ginkgo/v2 v2.19.0
|
||||||
github.com/onsi/gomega v1.33.1
|
github.com/onsi/gomega v1.33.1
|
||||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc
|
go.uber.org/zap v1.26.0
|
||||||
|
google.golang.org/grpc v1.65.0
|
||||||
|
google.golang.org/protobuf v1.34.2
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
k8s.io/api v0.31.0
|
k8s.io/api v0.31.0
|
||||||
k8s.io/apimachinery v0.31.0
|
k8s.io/apimachinery v0.31.0
|
||||||
|
@ -16,14 +21,19 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
cel.dev/expr v0.15.0 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
|
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
github.com/blang/semver/v4 v4.0.0 // 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/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/emicklei/go-restful/v3 v3.11.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
|
||||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
@ -35,6 +45,7 @@ require (
|
||||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||||
github.com/go-openapi/swag v0.22.4 // indirect
|
github.com/go-openapi/swag v0.22.4 // 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/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
|
@ -51,15 +62,22 @@ require (
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/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/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/prometheus/client_golang v1.19.1 // indirect
|
github.com/prometheus/client_golang v1.19.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.55.0 // indirect
|
github.com/prometheus/common v0.55.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
github.com/spf13/cobra v1.8.1 // indirect
|
github.com/spf13/cobra v1.8.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||||
|
@ -73,21 +91,19 @@ require (
|
||||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.26.0 // indirect
|
golang.org/x/crypto v0.29.0 // indirect
|
||||||
golang.org/x/crypto v0.27.0 // indirect
|
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
|
||||||
golang.org/x/net v0.26.0 // indirect
|
golang.org/x/net v0.26.0 // indirect
|
||||||
golang.org/x/oauth2 v0.21.0 // indirect
|
golang.org/x/oauth2 v0.21.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.9.0 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
golang.org/x/sys v0.27.0 // indirect
|
||||||
golang.org/x/term v0.24.0 // indirect
|
golang.org/x/term v0.26.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.20.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // 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-20240528184218-531527333157 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||||
google.golang.org/grpc v1.65.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
k8s.io/apiextensions-apiserver v0.31.0 // indirect
|
k8s.io/apiextensions-apiserver v0.31.0 // indirect
|
||||||
|
|
59
go.sum
59
go.sum
|
@ -1,3 +1,11 @@
|
||||||
|
cel.dev/expr v0.15.0 h1:O1jzfJCQBfL5BFoYktaxwIhuttaQPsVWerH9/EEKx0w=
|
||||||
|
cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
|
||||||
|
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/kong v1.6.0 h1:mwOzbdMR7uv2vul9J0FU3GYxE7ls/iX1ieMg5WIM6gE=
|
||||||
|
github.com/alecthomas/kong v1.6.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
|
||||||
|
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/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||||
|
@ -8,16 +16,26 @@ 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/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/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.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/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
|
||||||
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||||
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
|
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
|
||||||
|
@ -44,6 +62,8 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB
|
||||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
|
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
|
@ -66,6 +86,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
@ -91,6 +113,18 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||||
|
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||||
|
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5/Jo=
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo=
|
||||||
|
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||||
|
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
@ -108,6 +142,8 @@ github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
|
||||||
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
|
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
@ -122,6 +158,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
@ -133,6 +171,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
@ -168,8 +207,8 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
|
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
|
||||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
@ -185,19 +224,19 @@ golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|
458
internal/controller/apigateway_controller.go
Normal file
458
internal/controller/apigateway_controller.go
Normal file
|
@ -0,0 +1,458 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||||
|
|
||||||
|
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
templates *template.Template
|
||||||
|
//go:embed templates/*.tmpl
|
||||||
|
templateFS embed.FS
|
||||||
|
ErrNoJwksConfigured = errors.New("no JWKS configured")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
jwksSecretNameField = ".spec.jwks.name"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
templates = template.Must(template.ParseFS(templateFS, "templates/*.tmpl"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIGatewayReconciler reconciles a APIGateway object
|
||||||
|
type APIGatewayReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||||
|
// move the current state of the cluster closer to the desired state.
|
||||||
|
//
|
||||||
|
// For more details, check Reconcile and its Result here:
|
||||||
|
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/reconcile
|
||||||
|
func (r *APIGatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
|
||||||
|
var (
|
||||||
|
gateway supabasev1alpha1.APIGateway
|
||||||
|
logger = log.FromContext(ctx)
|
||||||
|
envoyConfigHash, jwksHash string
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.Info("Reconciling APIGateway")
|
||||||
|
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &gateway); client.IgnoreNotFound(err) != nil {
|
||||||
|
logger.Error(err, "unable to fetch Gateway")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if jwksHash, err = r.reconcileJwksSecret(ctx, &gateway); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if envoyConfigHash, err = r.reconcileEnvoyConfig(ctx, &gateway); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.reconileEnvoyDeployment(ctx, &gateway, envoyConfigHash, jwksHash); err != nil {
|
||||||
|
if client.IgnoreNotFound(err) == nil {
|
||||||
|
logger.Error(err, "expected resource does not exist (yet), waiting for it to be present")
|
||||||
|
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.reconcileEnvoyService(ctx, &gateway); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *APIGatewayReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
|
||||||
|
err := mgr.GetFieldIndexer().IndexField(ctx, new(supabasev1alpha1.APIGateway), jwksSecretNameField, func(o client.Object) []string {
|
||||||
|
gw, ok := o.(*supabasev1alpha1.APIGateway)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{gw.Spec.JWKSSelector.Name}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting up field index for JWKS secret name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadSelector, err := predicate.LabelSelectorPredicate(metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{
|
||||||
|
meta.SupabaseLabel.Reload: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("constructor selector for watching secrets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&supabasev1alpha1.APIGateway{}).
|
||||||
|
Named("apigateway").
|
||||||
|
Owns(new(corev1.ConfigMap)).
|
||||||
|
Owns(new(appsv1.Deployment)).
|
||||||
|
Owns(new(corev1.Service)).
|
||||||
|
Watches(
|
||||||
|
new(corev1.Secret),
|
||||||
|
FieldSelectorEventHandler[*supabasev1alpha1.APIGateway, *supabasev1alpha1.APIGatewayList](r.Client,
|
||||||
|
jwksSecretNameField,
|
||||||
|
),
|
||||||
|
builder.WithPredicates(
|
||||||
|
predicate.ResourceVersionChangedPredicate{},
|
||||||
|
reloadSelector,
|
||||||
|
),
|
||||||
|
).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *APIGatewayReconciler) reconcileJwksSecret(
|
||||||
|
ctx context.Context,
|
||||||
|
gateway *supabasev1alpha1.APIGateway,
|
||||||
|
) (jwksHash string, err error) {
|
||||||
|
jwksSecret := &corev1.Secret{ObjectMeta: gateway.JwksSecretMeta()}
|
||||||
|
|
||||||
|
if err := r.Get(ctx, client.ObjectKeyFromObject(jwksSecret), jwksSecret); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
jwksRaw, ok := jwksSecret.Data[gateway.Spec.JWKSSelector.Key]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("%w in secret %s", ErrNoJwksConfigured, jwksSecret.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(HashBytes(jwksRaw)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *APIGatewayReconciler) reconcileEnvoyConfig(
|
||||||
|
ctx context.Context,
|
||||||
|
gateway *supabasev1alpha1.APIGateway,
|
||||||
|
) (configHash string, err error) {
|
||||||
|
configMap := &corev1.ConfigMap{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: supabase.ServiceConfig.Envoy.ObjectName(gateway),
|
||||||
|
Namespace: gateway.Namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, configMap, func() error {
|
||||||
|
configMap.Labels = MergeLabels(objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Postgrest.Tag), gateway.Labels)
|
||||||
|
|
||||||
|
type nodeSpec struct {
|
||||||
|
Cluster string
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type controlPlaneSpec struct {
|
||||||
|
Name string
|
||||||
|
Host string
|
||||||
|
Port uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := fmt.Sprintf("%s:%s", gateway.Name, gateway.Namespace)
|
||||||
|
|
||||||
|
tmplData := struct {
|
||||||
|
Node nodeSpec
|
||||||
|
ControlPlane controlPlaneSpec
|
||||||
|
}{
|
||||||
|
Node: nodeSpec{
|
||||||
|
ID: instance,
|
||||||
|
Cluster: instance,
|
||||||
|
},
|
||||||
|
ControlPlane: controlPlaneSpec{
|
||||||
|
Name: "supabase-control-plane",
|
||||||
|
Host: gateway.Spec.Envoy.ControlPlane.Host,
|
||||||
|
Port: gateway.Spec.Envoy.ControlPlane.Port,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesBuf := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
if err := templates.ExecuteTemplate(bytesBuf, "envoy_control_plane_config.yaml.tmpl", tmplData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configMap.Data = map[string]string{
|
||||||
|
"config.yaml": bytesBuf.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := controllerutil.SetControllerReference(gateway, configMap, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
configHash = hex.EncodeToString(HashStrings(configMap.Data[supabase.ServiceConfig.Envoy.Defaults.ConfigKey]))
|
||||||
|
|
||||||
|
return configHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *APIGatewayReconciler) reconileEnvoyDeployment(
|
||||||
|
ctx context.Context,
|
||||||
|
gateway *supabasev1alpha1.APIGateway,
|
||||||
|
configHash, jwksHash string,
|
||||||
|
) error {
|
||||||
|
envoyDeployment := &appsv1.Deployment{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: supabase.ServiceConfig.Envoy.ObjectName(gateway),
|
||||||
|
Namespace: gateway.Namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
envoySpec := gateway.Spec.Envoy
|
||||||
|
|
||||||
|
if envoySpec == nil {
|
||||||
|
envoySpec = new(supabasev1alpha1.EnvoySpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
if envoySpec.WorkloadTemplate == nil {
|
||||||
|
envoySpec.WorkloadTemplate = new(supabasev1alpha1.WorkloadTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if envoySpec.WorkloadTemplate.Workload == nil {
|
||||||
|
envoySpec.WorkloadTemplate.Workload = new(supabasev1alpha1.ContainerTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
image = supabase.Images.Envoy.String()
|
||||||
|
podSecurityContext = envoySpec.WorkloadTemplate.SecurityContext
|
||||||
|
pullPolicy = envoySpec.WorkloadTemplate.Workload.PullPolicy
|
||||||
|
containerSecurityContext = envoySpec.WorkloadTemplate.Workload.SecurityContext
|
||||||
|
)
|
||||||
|
|
||||||
|
if img := envoySpec.WorkloadTemplate.Workload.Image; img != "" {
|
||||||
|
image = img
|
||||||
|
}
|
||||||
|
|
||||||
|
if podSecurityContext == nil {
|
||||||
|
podSecurityContext = &corev1.PodSecurityContext{
|
||||||
|
RunAsNonRoot: ptrOf(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerSecurityContext == nil {
|
||||||
|
containerSecurityContext = &corev1.SecurityContext{
|
||||||
|
Privileged: ptrOf(false),
|
||||||
|
RunAsUser: ptrOf(int64(65532)),
|
||||||
|
RunAsGroup: ptrOf(int64(65532)),
|
||||||
|
RunAsNonRoot: ptrOf(true),
|
||||||
|
AllowPrivilegeEscalation: ptrOf(false),
|
||||||
|
ReadOnlyRootFilesystem: ptrOf(true),
|
||||||
|
Capabilities: &corev1.Capabilities{
|
||||||
|
Drop: []corev1.Capability{
|
||||||
|
"ALL",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, envoyDeployment, func() error {
|
||||||
|
envoyDeployment.Labels = MergeLabels(
|
||||||
|
objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Postgrest.Tag),
|
||||||
|
gateway.Labels,
|
||||||
|
envoySpec.WorkloadTemplate.AdditionalLabels,
|
||||||
|
)
|
||||||
|
|
||||||
|
if envoyDeployment.CreationTimestamp.IsZero() {
|
||||||
|
envoyDeployment.Spec.Selector = &metav1.LabelSelector{
|
||||||
|
MatchLabels: selectorLabels(gateway, "envoy"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
envoyDeployment.Spec.Replicas = envoySpec.WorkloadTemplate.Replicas
|
||||||
|
|
||||||
|
envoyDeployment.Spec.Template = corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
fmt.Sprintf("%s/%s", supabasev1alpha1.GroupVersion.Group, "config-hash"): configHash,
|
||||||
|
fmt.Sprintf("%s/%s", supabasev1alpha1.GroupVersion.Group, "jwks-hash"): jwksHash,
|
||||||
|
},
|
||||||
|
Labels: MergeLabels(
|
||||||
|
objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Envoy.Tag),
|
||||||
|
envoySpec.WorkloadTemplate.AdditionalLabels,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
ImagePullSecrets: envoySpec.WorkloadTemplate.Workload.ImagePullSecrets,
|
||||||
|
AutomountServiceAccountToken: ptrOf(false),
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "envoy-proxy",
|
||||||
|
Image: image,
|
||||||
|
ImagePullPolicy: pullPolicy,
|
||||||
|
Args: []string{"-c /etc/envoy/config.yaml"},
|
||||||
|
Ports: []corev1.ContainerPort{
|
||||||
|
{
|
||||||
|
Name: "http",
|
||||||
|
ContainerPort: 8000,
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "admin",
|
||||||
|
ContainerPort: 19000,
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReadinessProbe: &corev1.Probe{
|
||||||
|
InitialDelaySeconds: 5,
|
||||||
|
PeriodSeconds: 3,
|
||||||
|
TimeoutSeconds: 1,
|
||||||
|
SuccessThreshold: 2,
|
||||||
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
|
Path: "/ready",
|
||||||
|
Port: intstr.IntOrString{IntVal: 19000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LivenessProbe: &corev1.Probe{
|
||||||
|
InitialDelaySeconds: 10,
|
||||||
|
PeriodSeconds: 5,
|
||||||
|
TimeoutSeconds: 3,
|
||||||
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
|
Path: "/ready",
|
||||||
|
Port: intstr.IntOrString{IntVal: 19000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SecurityContext: containerSecurityContext,
|
||||||
|
Resources: envoySpec.WorkloadTemplate.Workload.Resources,
|
||||||
|
VolumeMounts: []corev1.VolumeMount{
|
||||||
|
{
|
||||||
|
Name: "config",
|
||||||
|
ReadOnly: true,
|
||||||
|
MountPath: "/etc/envoy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SecurityContext: podSecurityContext,
|
||||||
|
Volumes: []corev1.Volume{
|
||||||
|
{
|
||||||
|
Name: "config",
|
||||||
|
VolumeSource: corev1.VolumeSource{
|
||||||
|
Projected: &corev1.ProjectedVolumeSource{
|
||||||
|
Sources: []corev1.VolumeProjection{
|
||||||
|
{
|
||||||
|
ConfigMap: &corev1.ConfigMapProjection{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: supabase.ServiceConfig.Envoy.ObjectName(gateway),
|
||||||
|
},
|
||||||
|
Items: []corev1.KeyToPath{{
|
||||||
|
Key: "config.yaml",
|
||||||
|
Path: "config.yaml",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Secret: &corev1.SecretProjection{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: gateway.Spec.JWKSSelector.Name,
|
||||||
|
},
|
||||||
|
Items: []corev1.KeyToPath{{
|
||||||
|
Key: gateway.Spec.JWKSSelector.Key,
|
||||||
|
Path: "jwks.json",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := controllerutil.SetControllerReference(gateway, envoyDeployment, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *APIGatewayReconciler) reconcileEnvoyService(
|
||||||
|
ctx context.Context,
|
||||||
|
gateway *supabasev1alpha1.APIGateway,
|
||||||
|
) error {
|
||||||
|
envoyService := &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: supabase.ServiceConfig.Envoy.ObjectName(gateway),
|
||||||
|
Namespace: gateway.Namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, envoyService, func() error {
|
||||||
|
envoyService.Labels = MergeLabels(objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Postgrest.Tag), gateway.Labels)
|
||||||
|
|
||||||
|
envoyService.Spec = corev1.ServiceSpec{
|
||||||
|
Selector: selectorLabels(gateway, "postgrest"),
|
||||||
|
Ports: []corev1.ServicePort{
|
||||||
|
{
|
||||||
|
Name: "rest",
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
AppProtocol: ptrOf("http"),
|
||||||
|
Port: 8000,
|
||||||
|
TargetPort: intstr.IntOrString{IntVal: 8000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := controllerutil.SetControllerReference(gateway, envoyService, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
84
internal/controller/apigateway_controller_test.go
Normal file
84
internal/controller/apigateway_controller_test.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("APIGateway Controller", func() {
|
||||||
|
Context("When reconciling a resource", func() {
|
||||||
|
const resourceName = "test-resource"
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
typeNamespacedName := types.NamespacedName{
|
||||||
|
Name: resourceName,
|
||||||
|
Namespace: "default", // TODO(user):Modify as needed
|
||||||
|
}
|
||||||
|
apigateway := &supabasev1alpha1.APIGateway{}
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
By("creating the custom resource for the Kind APIGateway")
|
||||||
|
err := k8sClient.Get(ctx, typeNamespacedName, apigateway)
|
||||||
|
if err != nil && errors.IsNotFound(err) {
|
||||||
|
resource := &supabasev1alpha1.APIGateway{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: resourceName,
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
// TODO(user): Specify other spec details if needed.
|
||||||
|
}
|
||||||
|
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
// TODO(user): Cleanup logic after each test, like removing the resource instance.
|
||||||
|
resource := &supabasev1alpha1.APIGateway{}
|
||||||
|
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("Cleanup the specific resource instance APIGateway")
|
||||||
|
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||||
|
})
|
||||||
|
It("should successfully reconcile the resource", func() {
|
||||||
|
By("Reconciling the created resource")
|
||||||
|
controllerReconciler := &APIGatewayReconciler{
|
||||||
|
Client: k8sClient,
|
||||||
|
Scheme: k8sClient.Scheme(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
|
||||||
|
NamespacedName: typeNamespacedName,
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
|
||||||
|
// Example: If you expect a certain status condition after reconciliation, verify it here.
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,132 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 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 controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
|
||||||
|
|
||||||
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
|
||||||
"code.icb4dc0.de/prskr/supabase-operator/assets/migrations"
|
|
||||||
"code.icb4dc0.de/prskr/supabase-operator/infrastructure/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CoreReconciler reconciles a Core object
|
|
||||||
type CoreReconciler struct {
|
|
||||||
client.Client
|
|
||||||
Scheme *runtime.Scheme
|
|
||||||
}
|
|
||||||
|
|
||||||
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=cores,verbs=get;list;watch;create;update;patch;delete
|
|
||||||
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=cores/status,verbs=get;update;patch
|
|
||||||
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=cores/finalizers,verbs=update
|
|
||||||
|
|
||||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
|
||||||
// move the current state of the cluster closer to the desired state.
|
|
||||||
// TODO(user): Modify the Reconcile function to compare the state specified by
|
|
||||||
// the Core object against the actual cluster state, and then
|
|
||||||
// perform operations to make the cluster state reflect the state specified by
|
|
||||||
// the user.
|
|
||||||
//
|
|
||||||
// For more details, check Reconcile and its Result here:
|
|
||||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/reconcile
|
|
||||||
func (r *CoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
|
|
||||||
logger := log.FromContext(ctx)
|
|
||||||
|
|
||||||
var core supabasev1alpha1.Core
|
|
||||||
|
|
||||||
if err := r.Get(ctx, req.NamespacedName, &core); client.IgnoreNotFound(err) != nil {
|
|
||||||
logger.Error(err, "unable to fetch Core")
|
|
||||||
return ctrl.Result{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dsn, err := core.Spec.Database.GetDSN(ctx, client.NewNamespacedClient(r.Client, req.Namespace))
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err, "unable to get DSN")
|
|
||||||
return ctrl.Result{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := pgx.Connect(ctx, dsn)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err, "unable to connect to database")
|
|
||||||
return ctrl.Result{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer CloseCtx(ctx, conn, &err)
|
|
||||||
|
|
||||||
if err := r.applyMissingMigrations(ctx, conn, &core); err != nil {
|
|
||||||
return ctrl.Result{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctrl.Result{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupWithManager sets up the controller with the Manager.
|
|
||||||
func (r *CoreReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
|
||||||
return ctrl.NewControllerManagedBy(mgr).
|
|
||||||
For(&supabasev1alpha1.Core{}).
|
|
||||||
Owns(new(appsv1.Deployment)).
|
|
||||||
Named("core").
|
|
||||||
Complete(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CoreReconciler) applyMissingMigrations(ctx context.Context, conn *pgx.Conn, core *supabasev1alpha1.Core) (err error) {
|
|
||||||
logger := log.FromContext(ctx)
|
|
||||||
logger.Info("Checking for outstanding migrations")
|
|
||||||
migrator := db.Migrator{Conn: conn}
|
|
||||||
|
|
||||||
var appliedSomething bool
|
|
||||||
|
|
||||||
if appliedSomething, err = migrator.ApplyAll(ctx, core.Status.AppliedMigrations, migrations.InitScripts()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if appliedSomething {
|
|
||||||
logger.Info("Updating status after applying init scripts")
|
|
||||||
return r.Client.Status().Update(ctx, core)
|
|
||||||
}
|
|
||||||
|
|
||||||
if appliedSomething, err = migrator.ApplyAll(ctx, core.Status.AppliedMigrations, migrations.MigrationScripts()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if appliedSomething {
|
|
||||||
logger.Info("Updating status after applying migration scripts")
|
|
||||||
return r.Client.Status().Update(ctx, core)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Close(closer io.Closer, err *error) {
|
|
||||||
*err = errors.Join(*err, closer.Close())
|
|
||||||
}
|
|
||||||
|
|
||||||
func CloseCtx(ctx context.Context, closable interface {
|
|
||||||
Close(ctx context.Context) error
|
|
||||||
}, err *error,
|
|
||||||
) {
|
|
||||||
*err = errors.Join(*err, closable.Close(ctx))
|
|
||||||
}
|
|
255
internal/controller/core_db_controller.go
Normal file
255
internal/controller/core_db_controller.go
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"maps"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/assets/migrations"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/db"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoreDbReconciler reconciles a Core object
|
||||||
|
type CoreDbReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CoreDbReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
var core supabasev1alpha1.Core
|
||||||
|
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &core); client.IgnoreNotFound(err) != nil {
|
||||||
|
logger.Error(err, "unable to fetch Core")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn, err := core.Spec.Database.GetDSN(ctx, client.NewNamespacedClient(r.Client, req.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err, "unable to get DSN")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Connecting to database")
|
||||||
|
conn, err := pgx.Connect(ctx, dsn)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err, "unable to connect to database")
|
||||||
|
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer CloseCtx(ctx, conn, &err)
|
||||||
|
|
||||||
|
logger.Info("Connected to database, checking for outstanding migrations")
|
||||||
|
if err := r.applyMissingMigrations(ctx, conn, &core); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Sync credentials for Supabase roles")
|
||||||
|
if err := r.ensureDbRolesSecrets(ctx, dsn, conn, &core); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *CoreDbReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(new(supabasev1alpha1.Core)).
|
||||||
|
Owns(new(corev1.Secret)).
|
||||||
|
Named("core-db").
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CoreDbReconciler) applyMissingMigrations(
|
||||||
|
ctx context.Context,
|
||||||
|
conn *pgx.Conn,
|
||||||
|
core *supabasev1alpha1.Core,
|
||||||
|
) (err error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
migrator := db.Migrator{Conn: conn}
|
||||||
|
|
||||||
|
var appliedSomething bool
|
||||||
|
|
||||||
|
if core.Status.Database.AppliedMigrations == nil {
|
||||||
|
core.Status.Database.AppliedMigrations = make(supabasev1alpha1.MigrationStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
if appliedSomething, err = migrator.ApplyAll(ctx, core.Status.Database.AppliedMigrations, migrations.InitScripts()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if appliedSomething {
|
||||||
|
logger.Info("Updating status after applying init scripts")
|
||||||
|
return r.Client.Status().Update(ctx, core)
|
||||||
|
} else {
|
||||||
|
logger.Info("Init scripts were up to date - did not run any")
|
||||||
|
}
|
||||||
|
|
||||||
|
if appliedSomething, err = migrator.ApplyAll(ctx, core.Status.Database.AppliedMigrations, migrations.MigrationScripts()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if appliedSomething {
|
||||||
|
logger.Info("Updating status after applying migration scripts")
|
||||||
|
return r.Client.Status().Update(ctx, core)
|
||||||
|
} else {
|
||||||
|
logger.Info("Migrrations were up to date - did not run any")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CoreDbReconciler) ensureDbRolesSecrets(
|
||||||
|
ctx context.Context,
|
||||||
|
dsn string,
|
||||||
|
conn *pgx.Conn,
|
||||||
|
core *supabasev1alpha1.Core,
|
||||||
|
) error {
|
||||||
|
var (
|
||||||
|
logger = log.FromContext(ctx)
|
||||||
|
rolesMgr = db.NewRolesManager(conn)
|
||||||
|
)
|
||||||
|
|
||||||
|
dbSpec := core.Spec.Database
|
||||||
|
if dbSpec.Roles.SelfManaged {
|
||||||
|
logger.Info("Database roles are self-managed, skipping reconciliation")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedDSN, err := url.Parse(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
dsnUser = parsedDSN.User.Username()
|
||||||
|
dsnPW, _ = parsedDSN.User.Password()
|
||||||
|
)
|
||||||
|
|
||||||
|
roles := map[string]supabase.DBRole{
|
||||||
|
dbSpec.Roles.Secrets.Authenticator.Name: supabase.DBRoleAuthenticator,
|
||||||
|
dbSpec.Roles.Secrets.AuthAdmin.Name: supabase.DBRoleAuthAdmin,
|
||||||
|
dbSpec.Roles.Secrets.FunctionsAdmin.Name: supabase.DBRoleFunctionsAdmin,
|
||||||
|
dbSpec.Roles.Secrets.StorageAdmin.Name: supabase.DBRoleStorageAdmin,
|
||||||
|
dbSpec.Roles.Secrets.Admin.Name: supabase.DBRoleSupabaseAdmin,
|
||||||
|
}
|
||||||
|
|
||||||
|
if core.Status.Database.Roles == nil {
|
||||||
|
core.Status.Database.Roles = make(map[string][]byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
|
||||||
|
for secretName, role := range roles {
|
||||||
|
secretLogger := logger.WithValues("secret_name", secretName, "role_name", role.String())
|
||||||
|
|
||||||
|
secretLogger.Info("Ensuring credential secret")
|
||||||
|
|
||||||
|
credentialsSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: secretName,
|
||||||
|
Namespace: core.Namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, credentialsSecret, func() error {
|
||||||
|
logger.Info("Ensuring role credentials", "role_name", role.String())
|
||||||
|
|
||||||
|
credentialsSecret.Labels = maps.Clone(core.Labels)
|
||||||
|
if credentialsSecret.Labels == nil {
|
||||||
|
credentialsSecret.Labels = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
credentialsSecret.Labels[meta.SupabaseLabel.Reload] = ""
|
||||||
|
|
||||||
|
if credentialsSecret.Data == nil {
|
||||||
|
credentialsSecret.Data = make(map[string][]byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := credentialsSecret.Data[corev1.BasicAuthUsernameKey]; !ok {
|
||||||
|
credentialsSecret.Data[corev1.BasicAuthUsernameKey] = role.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
var requireStatusUpdate bool
|
||||||
|
|
||||||
|
if value := credentialsSecret.Data[corev1.BasicAuthPasswordKey]; len(value) == 0 || (role.String() == dsnUser && !bytes.Equal(credentialsSecret.Data[corev1.BasicAuthPasswordKey], []byte(dsnPW))) {
|
||||||
|
if role.String() == dsnUser {
|
||||||
|
credentialsSecret.Data[corev1.BasicAuthPasswordKey] = []byte(dsnPW)
|
||||||
|
} else {
|
||||||
|
credentialsSecret.Data[corev1.BasicAuthPasswordKey] = GeneratePW(24, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretLogger.Info("Update database role to match secret credentials")
|
||||||
|
if err := rolesMgr.UpdateRolePassword(ctx, role.String(), credentialsSecret.Data[corev1.BasicAuthPasswordKey]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
core.Status.Database.Roles[role.String()] = hash.Sum(credentialsSecret.Data[corev1.BasicAuthPasswordKey])
|
||||||
|
requireStatusUpdate = true
|
||||||
|
} else {
|
||||||
|
if bytes.Equal(core.Status.Database.Roles[role.String()], hash.Sum(credentialsSecret.Data[corev1.BasicAuthPasswordKey])) {
|
||||||
|
logger.Info("Role password is up to date", "role_name", role.String())
|
||||||
|
} else {
|
||||||
|
if err := rolesMgr.UpdateRolePassword(ctx, role.String(), credentialsSecret.Data[corev1.BasicAuthPasswordKey]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
requireStatusUpdate = true
|
||||||
|
}
|
||||||
|
core.Status.Database.Roles[role.String()] = hash.Sum(credentialsSecret.Data[corev1.BasicAuthPasswordKey])
|
||||||
|
}
|
||||||
|
|
||||||
|
credentialsSecret.Type = corev1.SecretTypeBasicAuth
|
||||||
|
|
||||||
|
if requireStatusUpdate {
|
||||||
|
secretLogger.Info("Updating status")
|
||||||
|
if err := r.Status().Update(ctx, core); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Setting owner reference for credentials secret")
|
||||||
|
if err := controllerutil.SetControllerReference(core, credentialsSecret, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -68,7 +68,7 @@ var _ = Describe("Core Controller", func() {
|
||||||
})
|
})
|
||||||
It("should successfully reconcile the resource", func() {
|
It("should successfully reconcile the resource", func() {
|
||||||
By("Reconciling the created resource")
|
By("Reconciling the created resource")
|
||||||
controllerReconciler := &CoreReconciler{
|
controllerReconciler := &CoreDbReconciler{
|
||||||
Client: k8sClient,
|
Client: k8sClient,
|
||||||
Scheme: k8sClient.Scheme(),
|
Scheme: k8sClient.Scheme(),
|
||||||
}
|
}
|
288
internal/controller/core_gotrue_controller.go
Normal file
288
internal/controller/core_gotrue_controller.go
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CoreAuthReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CoreAuthReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
|
||||||
|
var (
|
||||||
|
core supabasev1alpha1.Core
|
||||||
|
logger = log.FromContext(ctx)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &core); client.IgnoreNotFound(err) != nil {
|
||||||
|
logger.Error(err, "unable to fetch Core")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.reconcileAuthDeployment(ctx, &core); err != nil {
|
||||||
|
if client.IgnoreNotFound(err) == nil {
|
||||||
|
logger.Error(err, "expected resource does not exist (yet), waiting for it to be present")
|
||||||
|
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.reconcileAuthService(ctx, &core); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *CoreAuthReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
// TODO watch changes in DB credentials secret
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(new(supabasev1alpha1.Core)).
|
||||||
|
Owns(new(appsv1.Deployment)).
|
||||||
|
Owns(new(corev1.Service)).
|
||||||
|
Named("core-auth").
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
||||||
|
ctx context.Context,
|
||||||
|
core *supabasev1alpha1.Core,
|
||||||
|
) error {
|
||||||
|
var (
|
||||||
|
authDeployment = &appsv1.Deployment{
|
||||||
|
ObjectMeta: supabase.ServiceConfig.Auth.ObjectMeta(core),
|
||||||
|
}
|
||||||
|
authSpec = core.Spec.Auth
|
||||||
|
svcCfg = supabase.ServiceConfig.Auth
|
||||||
|
)
|
||||||
|
|
||||||
|
if authSpec.WorkloadTemplate == nil {
|
||||||
|
authSpec.WorkloadTemplate = new(supabasev1alpha1.WorkloadTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if authSpec.WorkloadTemplate.Workload == nil {
|
||||||
|
authSpec.WorkloadTemplate.Workload = new(supabasev1alpha1.ContainerTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
image = supabase.Images.Gotrue.String()
|
||||||
|
podSecurityContext = authSpec.WorkloadTemplate.SecurityContext
|
||||||
|
pullPolicy = authSpec.WorkloadTemplate.Workload.PullPolicy
|
||||||
|
containerSecurityContext = authSpec.WorkloadTemplate.Workload.SecurityContext
|
||||||
|
namespacedClient = client.NewNamespacedClient(r.Client, core.Namespace)
|
||||||
|
)
|
||||||
|
|
||||||
|
if img := authSpec.WorkloadTemplate.Workload.Image; img != "" {
|
||||||
|
image = img
|
||||||
|
}
|
||||||
|
|
||||||
|
if podSecurityContext == nil {
|
||||||
|
podSecurityContext = &corev1.PodSecurityContext{
|
||||||
|
RunAsNonRoot: ptrOf(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerSecurityContext == nil {
|
||||||
|
containerSecurityContext = &corev1.SecurityContext{
|
||||||
|
Privileged: ptrOf(false),
|
||||||
|
RunAsUser: ptrOf(int64(1000)),
|
||||||
|
RunAsGroup: ptrOf(int64(1000)),
|
||||||
|
RunAsNonRoot: ptrOf(true),
|
||||||
|
AllowPrivilegeEscalation: ptrOf(false),
|
||||||
|
ReadOnlyRootFilesystem: ptrOf(true),
|
||||||
|
Capabilities: &corev1.Capabilities{
|
||||||
|
Drop: []corev1.Capability{
|
||||||
|
"ALL",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseDSN, err := core.Spec.Database.GetDSN(ctx, namespacedClient)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedDSN, err := url.Parse(databaseDSN)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse DB DSN: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, authDeployment, func() error {
|
||||||
|
authDeployment.Labels = MergeLabels(
|
||||||
|
objectLabels(core, "auth", "core", supabase.Images.Gotrue.Tag),
|
||||||
|
core.Labels,
|
||||||
|
)
|
||||||
|
|
||||||
|
authDbEnv := []corev1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "POSTGRES_PASSWORD",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: core.Spec.Database.Roles.Secrets.AuthAdmin.Name,
|
||||||
|
},
|
||||||
|
Key: corev1.BasicAuthPasswordKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: svcCfg.EnvKeys.DatabaseUrl,
|
||||||
|
Value: strings.TrimSuffix(fmt.Sprintf("postgres://%s:$(POSTGRES_PASSWORD)@%s%s?%s", supabase.DBRoleAuthAdmin, parsedDSN.Host, parsedDSN.Path, parsedDSN.Query().Encode()), "?"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
authEnv := append(authDbEnv,
|
||||||
|
svcCfg.EnvKeys.ApiHost.Var(svcCfg.Defaults.ApiHost),
|
||||||
|
svcCfg.EnvKeys.ApiPort.Var(svcCfg.Defaults.ApiPort),
|
||||||
|
svcCfg.EnvKeys.ApiExternalUrl.Var(authSpec.APIExternalURL),
|
||||||
|
svcCfg.EnvKeys.DBDriver.Var(svcCfg.Defaults.DbDriver),
|
||||||
|
svcCfg.EnvKeys.SiteUrl.Var(authSpec.SiteURL),
|
||||||
|
svcCfg.EnvKeys.AdditionalRedirectURLs.Var(authSpec.AdditionalRedirectUrls),
|
||||||
|
svcCfg.EnvKeys.DisableSignup.Var(boolValueOf(authSpec.DisableSignup)),
|
||||||
|
svcCfg.EnvKeys.JWTIssuer.Var(svcCfg.Defaults.JwtIssuer),
|
||||||
|
svcCfg.EnvKeys.JWTAdminRoles.Var(svcCfg.Defaults.JwtAdminRoles),
|
||||||
|
svcCfg.EnvKeys.JWTAudience.Var(svcCfg.Defaults.JwtAudience),
|
||||||
|
svcCfg.EnvKeys.JwtDefaultGroup.Var(svcCfg.Defaults.JwtDefaultGroupName),
|
||||||
|
svcCfg.EnvKeys.JwtExpiry.Var(ValueOrFallback(core.Spec.JWT.Expiry, supabase.ServiceConfig.JWT.Defaults.Expiry)),
|
||||||
|
svcCfg.EnvKeys.JwtSecret.Var(core.Spec.JWT.SecretKeySelector()),
|
||||||
|
svcCfg.EnvKeys.EmailSignupDisabled.Var(boolValueOf(authSpec.EmailSignupDisabled)),
|
||||||
|
svcCfg.EnvKeys.AnonymousUsersEnabled.Var(boolValueOf(authSpec.AnonymousUsersEnabled)),
|
||||||
|
)
|
||||||
|
|
||||||
|
authEnv = append(authEnv, authSpec.Providers.Vars(authSpec.APIExternalURL)...)
|
||||||
|
|
||||||
|
if authDeployment.CreationTimestamp.IsZero() {
|
||||||
|
authDeployment.Spec.Selector = &metav1.LabelSelector{
|
||||||
|
MatchLabels: selectorLabels(core, "auth"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authDeployment.Spec.Replicas = authSpec.WorkloadTemplate.Replicas
|
||||||
|
|
||||||
|
authDeployment.Spec.Template = corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: objectLabels(core, "auth", "core", supabase.Images.Gotrue.Tag),
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
ImagePullSecrets: authSpec.WorkloadTemplate.Workload.ImagePullSecrets,
|
||||||
|
InitContainers: []corev1.Container{{
|
||||||
|
Name: "migrations",
|
||||||
|
Image: image,
|
||||||
|
ImagePullPolicy: pullPolicy,
|
||||||
|
Command: []string{"/usr/local/bin/auth"},
|
||||||
|
Args: []string{"migrate"},
|
||||||
|
Env: authEnv,
|
||||||
|
SecurityContext: containerSecurityContext,
|
||||||
|
}},
|
||||||
|
Containers: []corev1.Container{{
|
||||||
|
Name: "supabase-auth",
|
||||||
|
Image: image,
|
||||||
|
ImagePullPolicy: pullPolicy,
|
||||||
|
Command: []string{"/usr/local/bin/auth"},
|
||||||
|
Args: []string{"serve"},
|
||||||
|
Env: MergeEnv(authEnv, authSpec.WorkloadTemplate.Workload.AdditionalEnv...),
|
||||||
|
Ports: []corev1.ContainerPort{{
|
||||||
|
Name: "api",
|
||||||
|
ContainerPort: 9999,
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
}},
|
||||||
|
SecurityContext: containerSecurityContext,
|
||||||
|
Resources: authSpec.WorkloadTemplate.Workload.Resources,
|
||||||
|
VolumeMounts: authSpec.WorkloadTemplate.Workload.VolumeMounts,
|
||||||
|
ReadinessProbe: &corev1.Probe{
|
||||||
|
InitialDelaySeconds: 5,
|
||||||
|
PeriodSeconds: 3,
|
||||||
|
TimeoutSeconds: 1,
|
||||||
|
SuccessThreshold: 2,
|
||||||
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
|
Path: "/health",
|
||||||
|
Port: intstr.IntOrString{IntVal: 9999},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LivenessProbe: &corev1.Probe{
|
||||||
|
InitialDelaySeconds: 10,
|
||||||
|
PeriodSeconds: 5,
|
||||||
|
TimeoutSeconds: 3,
|
||||||
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
|
Path: "/health",
|
||||||
|
Port: intstr.IntOrString{IntVal: 9999},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
SecurityContext: podSecurityContext,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := controllerutil.SetControllerReference(core, authDeployment, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CoreAuthReconciler) reconcileAuthService(
|
||||||
|
ctx context.Context,
|
||||||
|
core *supabasev1alpha1.Core,
|
||||||
|
) error {
|
||||||
|
authService := &corev1.Service{
|
||||||
|
ObjectMeta: supabase.ServiceConfig.Auth.ObjectMeta(core),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, authService, func() error {
|
||||||
|
authService.Labels = MergeLabels(
|
||||||
|
objectLabels(core, "auth", "core", supabase.Images.Gotrue.Tag),
|
||||||
|
core.Labels,
|
||||||
|
)
|
||||||
|
|
||||||
|
authService.Labels[meta.SupabaseLabel.EnvoyCluster] = core.Name
|
||||||
|
|
||||||
|
authService.Spec = corev1.ServiceSpec{
|
||||||
|
Selector: selectorLabels(core, "auth"),
|
||||||
|
Ports: []corev1.ServicePort{
|
||||||
|
{
|
||||||
|
Name: "api",
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
AppProtocol: ptrOf("http"),
|
||||||
|
Port: 9999,
|
||||||
|
TargetPort: intstr.IntOrString{IntVal: 9999},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := controllerutil.SetControllerReference(core, authService, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
166
internal/controller/core_jwt_controller.go
Normal file
166
internal/controller/core_jwt_controller.go
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||||
|
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/jwk"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoreDbReconciler reconciles a Core object
|
||||||
|
type CoreJwtReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CoreJwtReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
|
||||||
|
var (
|
||||||
|
core supabasev1alpha1.Core
|
||||||
|
logger = log.FromContext(ctx)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &core); client.IgnoreNotFound(err) != nil {
|
||||||
|
logger.Error(err, "unable to fetch Core")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: core.Spec.JWT.SecretRef.Name, Namespace: core.Namespace},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, jwtSecret, func() error {
|
||||||
|
const (
|
||||||
|
secretJwksAndDefaultJWTs = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
var modifiedSecret bool
|
||||||
|
|
||||||
|
jwtSecret.Labels = maps.Clone(core.Labels)
|
||||||
|
if jwtSecret.Labels == nil {
|
||||||
|
jwtSecret.Labels = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtSecret.Labels[meta.SupabaseLabel.Reload] = ""
|
||||||
|
|
||||||
|
if err := controllerutil.SetControllerReference(&core, jwtSecret, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if jwtSecret.Data == nil {
|
||||||
|
jwtSecret.Data = make(map[string][]byte, secretJwksAndDefaultJWTs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if secret does not contain the JWT secret as configured
|
||||||
|
if value := jwtSecret.Data[core.Spec.JWT.SecretKey]; len(value) == 0 {
|
||||||
|
logger.Info("Generating new JWT secret")
|
||||||
|
generatedSecret, err := supabase.RandomJWTSecret()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtSecret.Data[core.Spec.JWT.SecretKey] = generatedSecret
|
||||||
|
modifiedSecret = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if value := jwtSecret.Data[core.Spec.JWT.JwksKey]; len(value) == 0 || modifiedSecret {
|
||||||
|
keySet := jwk.Set[jwk.SymmetricKey]{
|
||||||
|
Keys: []jwk.SymmetricKey{{
|
||||||
|
Algorithm: jwk.AlgorithmHS256,
|
||||||
|
Key: jwtSecret.Data[core.Spec.JWT.SecretKey],
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedKeySet, err := json.Marshal(keySet)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshalling JWKS: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtSecret.Data[core.Spec.JWT.JwksKey] = serializedKeySet
|
||||||
|
}
|
||||||
|
|
||||||
|
if value := jwtSecret.Data[core.Spec.JWT.AnonKey]; len(value) == 0 || modifiedSecret {
|
||||||
|
anonKey, err := generateJwt("anon", jwtSecret.Data[core.Spec.JWT.SecretKey])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtSecret.Data[core.Spec.JWT.AnonKey] = anonKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if value := jwtSecret.Data[core.Spec.JWT.ServiceKey]; len(value) == 0 || modifiedSecret {
|
||||||
|
serviceKey, err := generateJwt("service_role", jwtSecret.Data[core.Spec.JWT.SecretKey])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtSecret.Data[core.Spec.JWT.ServiceKey] = serviceKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *CoreJwtReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(new(supabasev1alpha1.Core)).
|
||||||
|
Owns(new(corev1.Secret)).
|
||||||
|
Named("core-jwt").
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateJwt(role string, secret []byte) ([]byte, error) {
|
||||||
|
claims := map[string]any{
|
||||||
|
"role": role,
|
||||||
|
jwt.IssuerKey: "supabase",
|
||||||
|
jwt.IssuedAtKey: time.Now().Add(-30 * time.Second),
|
||||||
|
jwt.ExpirationKey: time.Now().Add(365 * 24 * time.Hour),
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.New()
|
||||||
|
|
||||||
|
for k, v := range claims {
|
||||||
|
if err := token.Set(k, v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwt.Sign(token, jwt.WithKey(jwa.HS256, secret))
|
||||||
|
}
|
289
internal/controller/core_postgrest_controller.go
Normal file
289
internal/controller/core_postgrest_controller.go
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CorePostgrestReconiler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CorePostgrestReconiler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
|
||||||
|
var (
|
||||||
|
core supabasev1alpha1.Core
|
||||||
|
logger = log.FromContext(ctx)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &core); client.IgnoreNotFound(err) != nil {
|
||||||
|
logger.Error(err, "unable to fetch Core")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.reconilePostgrestDeployment(ctx, &core); err != nil {
|
||||||
|
if client.IgnoreNotFound(err) == nil {
|
||||||
|
logger.Error(err, "expected resource does not exist (yet), waiting for it to be present")
|
||||||
|
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.reconcilePostgrestService(ctx, &core); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *CorePostgrestReconiler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
// TODO watch changes in DB credentials secret
|
||||||
|
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(new(supabasev1alpha1.Core)).
|
||||||
|
Owns(new(appsv1.Deployment)).
|
||||||
|
Owns(new(corev1.Service)).
|
||||||
|
Named("core-postgrest").
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
||||||
|
ctx context.Context,
|
||||||
|
core *supabasev1alpha1.Core,
|
||||||
|
) error {
|
||||||
|
var (
|
||||||
|
serviceCfg = supabase.ServiceConfig.Postgrest
|
||||||
|
postgrestDeployment = &appsv1.Deployment{
|
||||||
|
ObjectMeta: serviceCfg.ObjectMeta(core),
|
||||||
|
}
|
||||||
|
postgrestSpec = core.Spec.Postgrest
|
||||||
|
)
|
||||||
|
|
||||||
|
if postgrestSpec.WorkloadTemplate == nil {
|
||||||
|
postgrestSpec.WorkloadTemplate = new(supabasev1alpha1.WorkloadTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if postgrestSpec.WorkloadTemplate.Workload == nil {
|
||||||
|
postgrestSpec.WorkloadTemplate.Workload = new(supabasev1alpha1.ContainerTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
image = supabase.Images.Postgrest.String()
|
||||||
|
podSecurityContext = postgrestSpec.WorkloadTemplate.SecurityContext
|
||||||
|
pullPolicy = postgrestSpec.WorkloadTemplate.Workload.PullPolicy
|
||||||
|
containerSecurityContext = postgrestSpec.WorkloadTemplate.Workload.SecurityContext
|
||||||
|
anonRole = ValueOrFallback(postgrestSpec.AnonRole, serviceCfg.Defaults.AnonRole)
|
||||||
|
postgrestSchemas = ValueOrFallback(postgrestSpec.Schemas, serviceCfg.Defaults.Schemas)
|
||||||
|
jwtSecretHash string
|
||||||
|
namespacedClient = client.NewNamespacedClient(r.Client, core.Namespace)
|
||||||
|
)
|
||||||
|
|
||||||
|
if img := postgrestSpec.WorkloadTemplate.Workload.Image; img != "" {
|
||||||
|
image = img
|
||||||
|
}
|
||||||
|
|
||||||
|
if podSecurityContext == nil {
|
||||||
|
podSecurityContext = &corev1.PodSecurityContext{
|
||||||
|
RunAsNonRoot: ptrOf(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerSecurityContext == nil {
|
||||||
|
containerSecurityContext = &corev1.SecurityContext{
|
||||||
|
Privileged: ptrOf(false),
|
||||||
|
RunAsUser: ptrOf(int64(1000)),
|
||||||
|
RunAsGroup: ptrOf(int64(1000)),
|
||||||
|
RunAsNonRoot: ptrOf(true),
|
||||||
|
AllowPrivilegeEscalation: ptrOf(false),
|
||||||
|
ReadOnlyRootFilesystem: ptrOf(true),
|
||||||
|
Capabilities: &corev1.Capabilities{
|
||||||
|
Drop: []corev1.Capability{
|
||||||
|
"ALL",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseDSN, err := core.Spec.Database.GetDSN(ctx, namespacedClient)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedDSN, err := url.Parse(databaseDSN)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse DB DSN: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jwtSecret, err := core.Spec.JWT.GetJWTSecret(ctx, namespacedClient); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
jwtSecretHash = hex.EncodeToString(HashBytes(jwtSecret))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, postgrestDeployment, func() error {
|
||||||
|
postgrestDeployment.Labels = MergeLabels(
|
||||||
|
objectLabels(core, serviceCfg.Name, "core", supabase.Images.Postgrest.Tag),
|
||||||
|
core.Labels,
|
||||||
|
)
|
||||||
|
|
||||||
|
postgrestEnv := []corev1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "DB_CREDENTIALS_PASSWORD",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: core.Spec.Database.Roles.Secrets.Authenticator.Name,
|
||||||
|
},
|
||||||
|
Key: corev1.BasicAuthPasswordKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: serviceCfg.EnvKeys.DBUri,
|
||||||
|
Value: strings.TrimSuffix(fmt.Sprintf("postgres://%s:$(DB_CREDENTIALS_PASSWORD)@%s%s?%s", supabase.DBRoleAuthenticator, parsedDSN.Host, parsedDSN.Path, parsedDSN.Query().Encode()), "?"),
|
||||||
|
},
|
||||||
|
serviceCfg.EnvKeys.JWTSecret.Var(core.Spec.JWT.JwksKeySelector()),
|
||||||
|
serviceCfg.EnvKeys.Schemas.Var(postgrestSchemas),
|
||||||
|
serviceCfg.EnvKeys.AnonRole.Var(anonRole),
|
||||||
|
serviceCfg.EnvKeys.UseLegacyGucs.Var(false),
|
||||||
|
serviceCfg.EnvKeys.ExtraSearchPath.Var(serviceCfg.Defaults.ExtraSearchPath),
|
||||||
|
serviceCfg.EnvKeys.AppSettingsJWTSecret.Var(core.Spec.JWT.SecretKeySelector()),
|
||||||
|
serviceCfg.EnvKeys.AppSettingsJWTExpiry.Var(ValueOrFallback(core.Spec.JWT.Expiry, supabase.ServiceConfig.JWT.Defaults.Expiry)),
|
||||||
|
serviceCfg.EnvKeys.AdminServerPort.Var(3001),
|
||||||
|
}
|
||||||
|
|
||||||
|
if postgrestDeployment.CreationTimestamp.IsZero() {
|
||||||
|
postgrestDeployment.Spec.Selector = &metav1.LabelSelector{
|
||||||
|
MatchLabels: selectorLabels(core, serviceCfg.Name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postgrestDeployment.Spec.Replicas = postgrestSpec.WorkloadTemplate.Replicas
|
||||||
|
|
||||||
|
postgrestDeployment.Spec.Template = corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
fmt.Sprintf("%s/%s", supabasev1alpha1.GroupVersion.Group, "jwt-hash"): jwtSecretHash,
|
||||||
|
},
|
||||||
|
Labels: objectLabels(core, serviceCfg.Name, "core", supabase.Images.Postgrest.Tag),
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
ImagePullSecrets: postgrestSpec.WorkloadTemplate.Workload.ImagePullSecrets,
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "supabase-rest",
|
||||||
|
Image: image,
|
||||||
|
ImagePullPolicy: pullPolicy,
|
||||||
|
Args: []string{"postgrest"},
|
||||||
|
Env: MergeEnv(postgrestEnv, postgrestSpec.WorkloadTemplate.Workload.AdditionalEnv...),
|
||||||
|
Ports: []corev1.ContainerPort{
|
||||||
|
{
|
||||||
|
Name: "rest",
|
||||||
|
ContainerPort: 3000,
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "admin",
|
||||||
|
ContainerPort: 3001,
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SecurityContext: containerSecurityContext,
|
||||||
|
Resources: postgrestSpec.WorkloadTemplate.Workload.Resources,
|
||||||
|
VolumeMounts: postgrestSpec.WorkloadTemplate.Workload.VolumeMounts,
|
||||||
|
ReadinessProbe: &corev1.Probe{
|
||||||
|
InitialDelaySeconds: 5,
|
||||||
|
PeriodSeconds: 3,
|
||||||
|
TimeoutSeconds: 1,
|
||||||
|
SuccessThreshold: 2,
|
||||||
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
|
Path: "/ready",
|
||||||
|
Port: intstr.IntOrString{IntVal: 3001},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LivenessProbe: &corev1.Probe{
|
||||||
|
InitialDelaySeconds: 10,
|
||||||
|
PeriodSeconds: 5,
|
||||||
|
TimeoutSeconds: 3,
|
||||||
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
|
Path: "/live",
|
||||||
|
Port: intstr.IntOrString{IntVal: 3001},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SecurityContext: podSecurityContext,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := controllerutil.SetControllerReference(core, postgrestDeployment, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CorePostgrestReconiler) reconcilePostgrestService(
|
||||||
|
ctx context.Context,
|
||||||
|
core *supabasev1alpha1.Core,
|
||||||
|
) error {
|
||||||
|
postgrestService := &corev1.Service{
|
||||||
|
ObjectMeta: supabase.ServiceConfig.Postgrest.ObjectMeta(core),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, postgrestService, func() error {
|
||||||
|
postgrestService.Labels = MergeLabels(
|
||||||
|
objectLabels(core, supabase.ServiceConfig.Postgrest.Name, "core", supabase.Images.Postgrest.Tag),
|
||||||
|
core.Labels,
|
||||||
|
)
|
||||||
|
|
||||||
|
postgrestService.Labels[meta.SupabaseLabel.EnvoyCluster] = core.Name
|
||||||
|
|
||||||
|
postgrestService.Spec = corev1.ServiceSpec{
|
||||||
|
Selector: selectorLabels(core, supabase.ServiceConfig.Postgrest.Name),
|
||||||
|
Ports: []corev1.ServicePort{
|
||||||
|
{
|
||||||
|
Name: "rest",
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
AppProtocol: ptrOf("http"),
|
||||||
|
Port: 3000,
|
||||||
|
TargetPort: intstr.IntOrString{IntVal: 3000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := controllerutil.SetControllerReference(core, postgrestService, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
84
internal/controller/dashboard_controller_test.go
Normal file
84
internal/controller/dashboard_controller_test.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Dashboard Controller", func() {
|
||||||
|
Context("When reconciling a resource", func() {
|
||||||
|
const resourceName = "test-resource"
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
typeNamespacedName := types.NamespacedName{
|
||||||
|
Name: resourceName,
|
||||||
|
Namespace: "default", // TODO(user):Modify as needed
|
||||||
|
}
|
||||||
|
dashboard := &supabasev1alpha1.Dashboard{}
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
By("creating the custom resource for the Kind Dashboard")
|
||||||
|
err := k8sClient.Get(ctx, typeNamespacedName, dashboard)
|
||||||
|
if err != nil && errors.IsNotFound(err) {
|
||||||
|
resource := &supabasev1alpha1.Dashboard{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: resourceName,
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
// TODO(user): Specify other spec details if needed.
|
||||||
|
}
|
||||||
|
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
// TODO(user): Cleanup logic after each test, like removing the resource instance.
|
||||||
|
resource := &supabasev1alpha1.Dashboard{}
|
||||||
|
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("Cleanup the specific resource instance Dashboard")
|
||||||
|
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||||
|
})
|
||||||
|
It("should successfully reconcile the resource", func() {
|
||||||
|
By("Reconciling the created resource")
|
||||||
|
controllerReconciler := &DashboardReconciler{
|
||||||
|
Client: k8sClient,
|
||||||
|
Scheme: k8sClient.Scheme(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
|
||||||
|
NamespacedName: typeNamespacedName,
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
|
||||||
|
// Example: If you expect a certain status condition after reconciliation, verify it here.
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
273
internal/controller/dashboard_pg-meta_controller.go
Normal file
273
internal/controller/dashboard_pg-meta_controller.go
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DashboardPGMetaReconciler reconciles a Dashboard object
|
||||||
|
type DashboardPGMetaReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=dashboards,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=dashboards/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=dashboards/finalizers,verbs=update
|
||||||
|
|
||||||
|
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||||
|
// move the current state of the cluster closer to the desired state.
|
||||||
|
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||||
|
// the Dashboard object against the actual cluster state, and then
|
||||||
|
// perform operations to make the cluster state reflect the state specified by
|
||||||
|
// the user.
|
||||||
|
//
|
||||||
|
// For more details, check Reconcile and its Result here:
|
||||||
|
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/reconcile
|
||||||
|
func (r *DashboardPGMetaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
var (
|
||||||
|
dashboard supabasev1alpha1.Dashboard
|
||||||
|
logger = log.FromContext(ctx)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &dashboard); client.IgnoreNotFound(err) != nil {
|
||||||
|
logger.Error(err, "unable to fetch Dashboard")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.reconcilePGMetaDeployment(ctx, &dashboard); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.reconcilePGMetaService(ctx, &dashboard); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *DashboardPGMetaReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&supabasev1alpha1.Dashboard{}).
|
||||||
|
Owns(new(appsv1.Deployment)).
|
||||||
|
Owns(new(corev1.Service)).
|
||||||
|
Named("dashboard-pgmeta").
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
|
||||||
|
ctx context.Context,
|
||||||
|
dashboard *supabasev1alpha1.Dashboard,
|
||||||
|
) error {
|
||||||
|
var (
|
||||||
|
serviceCfg = supabase.ServiceConfig.PGMeta
|
||||||
|
pgMetaDeployment = &appsv1.Deployment{
|
||||||
|
ObjectMeta: serviceCfg.ObjectMeta(dashboard),
|
||||||
|
}
|
||||||
|
pgMetaSpec = dashboard.Spec.PGMeta
|
||||||
|
)
|
||||||
|
|
||||||
|
if pgMetaSpec == nil {
|
||||||
|
pgMetaSpec = new(supabasev1alpha1.PGMetaSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pgMetaSpec.WorkloadTemplate == nil {
|
||||||
|
pgMetaSpec.WorkloadTemplate = new(supabasev1alpha1.WorkloadTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pgMetaSpec.WorkloadTemplate.Workload == nil {
|
||||||
|
pgMetaSpec.WorkloadTemplate.Workload = new(supabasev1alpha1.ContainerTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
image = supabase.Images.PostgresMeta.String()
|
||||||
|
podSecurityContext = pgMetaSpec.WorkloadTemplate.SecurityContext
|
||||||
|
pullPolicy = pgMetaSpec.WorkloadTemplate.Workload.PullPolicy
|
||||||
|
containerSecurityContext = pgMetaSpec.WorkloadTemplate.Workload.SecurityContext
|
||||||
|
)
|
||||||
|
|
||||||
|
if img := pgMetaSpec.WorkloadTemplate.Workload.Image; img != "" {
|
||||||
|
image = img
|
||||||
|
}
|
||||||
|
|
||||||
|
if podSecurityContext == nil {
|
||||||
|
podSecurityContext = &corev1.PodSecurityContext{
|
||||||
|
RunAsNonRoot: ptrOf(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerSecurityContext == nil {
|
||||||
|
containerSecurityContext = &corev1.SecurityContext{
|
||||||
|
Privileged: ptrOf(false),
|
||||||
|
RunAsUser: ptrOf(int64(1000)),
|
||||||
|
RunAsGroup: ptrOf(int64(1000)),
|
||||||
|
RunAsNonRoot: ptrOf(true),
|
||||||
|
AllowPrivilegeEscalation: ptrOf(false),
|
||||||
|
ReadOnlyRootFilesystem: ptrOf(true),
|
||||||
|
Capabilities: &corev1.Capabilities{
|
||||||
|
Drop: []corev1.Capability{
|
||||||
|
"ALL",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dsnSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: dashboard.Spec.DBSpec.DBCredentialsRef.Name,
|
||||||
|
Namespace: dashboard.Namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := r.Get(ctx, client.ObjectKeyFromObject(dsnSecret), dsnSecret); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, pgMetaDeployment, func() error {
|
||||||
|
pgMetaDeployment.Labels = MergeLabels(
|
||||||
|
objectLabels(dashboard, serviceCfg.Name, "dashboard", supabase.Images.PostgresMeta.Tag),
|
||||||
|
dashboard.Labels,
|
||||||
|
)
|
||||||
|
|
||||||
|
if pgMetaDeployment.CreationTimestamp.IsZero() {
|
||||||
|
pgMetaDeployment.Spec.Selector = &metav1.LabelSelector{
|
||||||
|
MatchLabels: selectorLabels(dashboard, serviceCfg.Name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pgMetaDeployment.Spec.Replicas = pgMetaSpec.WorkloadTemplate.Replicas
|
||||||
|
|
||||||
|
pgMetaEnv := []corev1.EnvVar{
|
||||||
|
serviceCfg.EnvKeys.APIPort.Var(serviceCfg.Defaults.APIPort),
|
||||||
|
serviceCfg.EnvKeys.DBHost.Var(dashboard.Spec.DBSpec.Host),
|
||||||
|
serviceCfg.EnvKeys.DBPort.Var(dashboard.Spec.DBSpec.Port),
|
||||||
|
serviceCfg.EnvKeys.DBUser.Var(dashboard.Spec.DBSpec.UserRef()),
|
||||||
|
serviceCfg.EnvKeys.DBPassword.Var(dashboard.Spec.DBSpec.PasswordRef()),
|
||||||
|
}
|
||||||
|
|
||||||
|
pgMetaDeployment.Spec.Template = corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: objectLabels(dashboard, serviceCfg.Name, "dashboard", supabase.Images.PostgresMeta.Tag),
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
ImagePullSecrets: pgMetaSpec.WorkloadTemplate.Workload.ImagePullSecrets,
|
||||||
|
Containers: []corev1.Container{{
|
||||||
|
Name: "supabase-meta",
|
||||||
|
Image: image,
|
||||||
|
ImagePullPolicy: pullPolicy,
|
||||||
|
Env: MergeEnv(pgMetaEnv, pgMetaSpec.WorkloadTemplate.Workload.AdditionalEnv...),
|
||||||
|
Ports: []corev1.ContainerPort{{
|
||||||
|
Name: "api",
|
||||||
|
ContainerPort: int32(serviceCfg.Defaults.APIPort),
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
}},
|
||||||
|
SecurityContext: containerSecurityContext,
|
||||||
|
Resources: pgMetaSpec.WorkloadTemplate.Workload.Resources,
|
||||||
|
VolumeMounts: pgMetaSpec.WorkloadTemplate.Workload.VolumeMounts,
|
||||||
|
ReadinessProbe: &corev1.Probe{
|
||||||
|
InitialDelaySeconds: 5,
|
||||||
|
PeriodSeconds: 3,
|
||||||
|
TimeoutSeconds: 1,
|
||||||
|
SuccessThreshold: 2,
|
||||||
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
|
Path: "/health",
|
||||||
|
Port: intstr.IntOrString{IntVal: int32(serviceCfg.Defaults.APIPort)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LivenessProbe: &corev1.Probe{
|
||||||
|
InitialDelaySeconds: 10,
|
||||||
|
PeriodSeconds: 5,
|
||||||
|
TimeoutSeconds: 3,
|
||||||
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
|
Path: "/health",
|
||||||
|
Port: intstr.IntOrString{IntVal: int32(serviceCfg.Defaults.APIPort)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
SecurityContext: podSecurityContext,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := controllerutil.SetControllerReference(dashboard, pgMetaDeployment, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardPGMetaReconciler) reconcilePGMetaService(
|
||||||
|
ctx context.Context,
|
||||||
|
dashboard *supabasev1alpha1.Dashboard,
|
||||||
|
) error {
|
||||||
|
pgMetaService := &corev1.Service{
|
||||||
|
ObjectMeta: supabase.ServiceConfig.PGMeta.ObjectMeta(dashboard),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controllerutil.CreateOrPatch(ctx, r.Client, pgMetaService, func() error {
|
||||||
|
pgMetaService.Labels = MergeLabels(
|
||||||
|
objectLabels(dashboard, supabase.ServiceConfig.PGMeta.Name, "dashboard", supabase.Images.PostgresMeta.Tag),
|
||||||
|
dashboard.Labels,
|
||||||
|
)
|
||||||
|
|
||||||
|
pgMetaService.Labels[meta.SupabaseLabel.EnvoyCluster] = dashboard.Name
|
||||||
|
|
||||||
|
apiPort := int32(supabase.ServiceConfig.PGMeta.Defaults.APIPort)
|
||||||
|
|
||||||
|
pgMetaService.Spec = corev1.ServiceSpec{
|
||||||
|
Selector: selectorLabels(dashboard, supabase.ServiceConfig.PGMeta.Name),
|
||||||
|
Ports: []corev1.ServicePort{
|
||||||
|
{
|
||||||
|
Name: "api",
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
AppProtocol: ptrOf("http"),
|
||||||
|
Port: apiPort,
|
||||||
|
TargetPort: intstr.IntOrString{IntVal: apiPort},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := controllerutil.SetControllerReference(dashboard, pgMetaService, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
41
internal/controller/object_meta.go
Normal file
41
internal/controller/object_meta.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maps"
|
||||||
|
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
|
||||||
|
)
|
||||||
|
|
||||||
|
func selectorLabels(
|
||||||
|
object client.Object,
|
||||||
|
name string,
|
||||||
|
) map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
meta.WellKnownLabel.Name: name,
|
||||||
|
meta.WellKnownLabel.Instance: object.GetName(),
|
||||||
|
meta.WellKnownLabel.PartOf: "supabase",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func objectLabels(
|
||||||
|
object client.Object,
|
||||||
|
name,
|
||||||
|
component,
|
||||||
|
version string,
|
||||||
|
) map[string]string {
|
||||||
|
labels := maps.Clone(object.GetLabels())
|
||||||
|
if labels == nil {
|
||||||
|
labels = make(map[string]string, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
labels[meta.WellKnownLabel.Name] = name
|
||||||
|
labels[meta.WellKnownLabel.Instance] = object.GetName()
|
||||||
|
labels[meta.WellKnownLabel.Version] = version
|
||||||
|
labels[meta.WellKnownLabel.Component] = component
|
||||||
|
labels[meta.WellKnownLabel.PartOf] = "supabase"
|
||||||
|
labels[meta.WellKnownLabel.ManagedBy] = "supabase-operator"
|
||||||
|
|
||||||
|
return labels
|
||||||
|
}
|
11
internal/controller/permissions.go
Normal file
11
internal/controller/permissions.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=cores,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=cores/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=cores/finalizers,verbs=update
|
||||||
|
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=apigateways,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=apigateways/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=apigateways/finalizers,verbs=update
|
||||||
|
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups="",resources=secrets;configmaps;services,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups="",resources=events,verbs=create
|
33
internal/controller/pwgen.go
Normal file
33
internal/controller/pwgen.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"math/rand/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GeneratePW(length uint, random *rand.Rand) []byte {
|
||||||
|
var (
|
||||||
|
builder = bytes.NewBuffer(nil)
|
||||||
|
alphabet = runes('a', 'z') + runes('A', 'Z') + runes('0', '9')
|
||||||
|
)
|
||||||
|
|
||||||
|
if random == nil {
|
||||||
|
random = rand.New(rand.NewPCG(0, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
for range length {
|
||||||
|
builder.WriteRune(rune(alphabet[random.IntN(len(alphabet))]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runes(start, end rune) string {
|
||||||
|
result := make([]rune, 0, int(end-start))
|
||||||
|
|
||||||
|
for current := start; current != end; current++ {
|
||||||
|
result = append(result, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(result)
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
node:
|
||||||
|
cluster: {{ .Node.Cluster }}
|
||||||
|
id: {{ .Node.ID }}
|
||||||
|
|
||||||
|
dynamic_resources:
|
||||||
|
ads_config:
|
||||||
|
api_type: GRPC
|
||||||
|
grpc_services:
|
||||||
|
- envoy_grpc:
|
||||||
|
cluster_name: {{ .ControlPlane.Name }}
|
||||||
|
cds_config:
|
||||||
|
ads: {}
|
||||||
|
lds_config:
|
||||||
|
ads: {}
|
||||||
|
|
||||||
|
static_resources:
|
||||||
|
clusters:
|
||||||
|
- type: STRICT_DNS
|
||||||
|
typed_extension_protocol_options:
|
||||||
|
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
|
||||||
|
explicit_http_config:
|
||||||
|
http2_protocol_options: {}
|
||||||
|
name: {{ .ControlPlane.Name }}
|
||||||
|
load_assignment:
|
||||||
|
cluster_name: {{ .ControlPlane.Name }}
|
||||||
|
endpoints:
|
||||||
|
- lb_endpoints:
|
||||||
|
- endpoint:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: {{ .ControlPlane.Host }}
|
||||||
|
port_value: {{ .ControlPlane.Port }}
|
||||||
|
|
||||||
|
admin:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: 0.0.0.0
|
||||||
|
port_value: 19000
|
145
internal/controller/utils.go
Normal file
145
internal/controller/utils.go
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"maps"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Close(closer io.Closer, err *error) {
|
||||||
|
*err = errors.Join(*err, closer.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseCtx(ctx context.Context, closable interface {
|
||||||
|
Close(ctx context.Context) error
|
||||||
|
}, err *error,
|
||||||
|
) {
|
||||||
|
*err = errors.Join(*err, closable.Close(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptrOf[T any](val T) *T {
|
||||||
|
return &val
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolValueOf(ptr *bool) bool {
|
||||||
|
if ptr == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return *ptr
|
||||||
|
}
|
||||||
|
|
||||||
|
func MergeLabels(source map[string]string, toAppend ...map[string]string) map[string]string {
|
||||||
|
result := make(map[string]string, len(source)+len(toAppend))
|
||||||
|
maps.Copy(result, source)
|
||||||
|
|
||||||
|
for _, additionalLabels := range toAppend {
|
||||||
|
for k, v := range additionalLabels {
|
||||||
|
if _, exists := result[k]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func MergeEnv(source []corev1.EnvVar, toAppend ...corev1.EnvVar) []corev1.EnvVar {
|
||||||
|
existingKeys := make(map[string]bool, len(source)+len(toAppend))
|
||||||
|
|
||||||
|
merged := append(make([]corev1.EnvVar, 0, len(source)+len(toAppend)), source...)
|
||||||
|
|
||||||
|
for _, v := range source {
|
||||||
|
existingKeys[v.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range toAppend {
|
||||||
|
if _, alreadyPresent := existingKeys[v.Name]; alreadyPresent {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
merged = append(merged, v)
|
||||||
|
existingKeys[v.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValueOrFallback[T any](value, fallback T) T {
|
||||||
|
rval := reflect.ValueOf(value)
|
||||||
|
if rval.IsZero() {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func HashStrings(vals ...string) []byte {
|
||||||
|
h := sha256.New()
|
||||||
|
|
||||||
|
for _, v := range vals {
|
||||||
|
h.Write([]byte(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HashBytes(vals ...[]byte) []byte {
|
||||||
|
h := sha256.New()
|
||||||
|
|
||||||
|
for _, v := range vals {
|
||||||
|
h.Write(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FieldSelectorEventHandler[TItem metav1.Object, TList api.ObjectList[TItem]](
|
||||||
|
cli client.Client,
|
||||||
|
fieldSelector string,
|
||||||
|
) handler.TypedEventHandler[client.Object, reconcile.Request] {
|
||||||
|
return handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||||
|
var (
|
||||||
|
list TList
|
||||||
|
selectorInstance = client.MatchingFieldsSelector{
|
||||||
|
Selector: fields.OneTermEqualSelector(fieldSelector, obj.GetName()),
|
||||||
|
}
|
||||||
|
logger = log.FromContext(ctx, "object", obj.GetName(), "namespace", obj.GetNamespace())
|
||||||
|
)
|
||||||
|
|
||||||
|
list = reflect.New(reflect.TypeOf(list).Elem()).Interface().(TList)
|
||||||
|
|
||||||
|
if err := cli.List(ctx, list, selectorInstance, client.InNamespace(obj.GetNamespace())); err != nil {
|
||||||
|
logger.Error(err, "could not list items for field selector event handler")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
requests := make([]reconcile.Request, 0)
|
||||||
|
|
||||||
|
for item := range list.Iter() {
|
||||||
|
requests = append(requests, reconcile.Request{
|
||||||
|
NamespacedName: types.NamespacedName{
|
||||||
|
Name: item.GetName(),
|
||||||
|
Namespace: obj.GetNamespace(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return requests
|
||||||
|
})
|
||||||
|
}
|
209
internal/controlplane/auth_filters.go
Normal file
209
internal/controlplane/auth_filters.go
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
package controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
|
rbacv3cfg "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
|
||||||
|
jwtauthnv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3"
|
||||||
|
rbacv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
|
||||||
|
matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
JwtProviderName = "supabase"
|
||||||
|
JwtMetadataKey = "supabase-jwt"
|
||||||
|
JwtAuthenticatedRequirement = "supabase-jwt-authenticated"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ForwardJwt bool
|
||||||
|
|
||||||
|
func (j ForwardJwt) Apply(opts *JwtOptions) {
|
||||||
|
opts.ForwardJwt = bool(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
type JwtFilterOption interface {
|
||||||
|
Apply(opts *JwtOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
type JwtFilterOptionFunc func(opts *JwtOptions)
|
||||||
|
|
||||||
|
func (f JwtFilterOptionFunc) Apply(opts *JwtOptions) {
|
||||||
|
f(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
type JwtOptions struct {
|
||||||
|
ForwardJwt bool
|
||||||
|
ForwardHeader string
|
||||||
|
}
|
||||||
|
|
||||||
|
func JWTPerRouteConfig() *jwtauthnv3.PerRouteConfig {
|
||||||
|
return &jwtauthnv3.PerRouteConfig{
|
||||||
|
RequirementSpecifier: &jwtauthnv3.PerRouteConfig_RequirementName{
|
||||||
|
RequirementName: JwtAuthenticatedRequirement,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func JWTAllowAll() *jwtauthnv3.PerRouteConfig {
|
||||||
|
return &jwtauthnv3.PerRouteConfig{
|
||||||
|
RequirementSpecifier: &jwtauthnv3.PerRouteConfig_Disabled{
|
||||||
|
Disabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func JWTFilterConfig(opts ...JwtFilterOption) *jwtauthnv3.JwtAuthentication {
|
||||||
|
const (
|
||||||
|
issuerName = "supabase"
|
||||||
|
bearerTokenPrefix = "Bearer "
|
||||||
|
apiKeyParamKey = "apikey"
|
||||||
|
authorizationHeaderKey = "Authorization"
|
||||||
|
)
|
||||||
|
|
||||||
|
filterOpts := &JwtOptions{
|
||||||
|
ForwardJwt: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range opts {
|
||||||
|
o.Apply(filterOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &jwtauthnv3.JwtAuthentication{
|
||||||
|
Providers: map[string]*jwtauthnv3.JwtProvider{
|
||||||
|
JwtProviderName: {
|
||||||
|
Issuer: issuerName,
|
||||||
|
PayloadInMetadata: JwtMetadataKey,
|
||||||
|
JwksSourceSpecifier: &jwtauthnv3.JwtProvider_LocalJwks{
|
||||||
|
LocalJwks: &corev3.DataSource{
|
||||||
|
Specifier: &corev3.DataSource_Filename{
|
||||||
|
Filename: "/etc/envoy/jwks.json",
|
||||||
|
},
|
||||||
|
WatchedDirectory: &corev3.WatchedDirectory{
|
||||||
|
Path: "/etc/envoy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Forward: filterOpts.ForwardJwt,
|
||||||
|
FromHeaders: []*jwtauthnv3.JwtHeader{
|
||||||
|
{
|
||||||
|
Name: apiKeyParamKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: authorizationHeaderKey,
|
||||||
|
ValuePrefix: bearerTokenPrefix,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FromParams: []string{apiKeyParamKey},
|
||||||
|
RequireExpiration: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
BypassCorsPreflight: true,
|
||||||
|
RequirementMap: map[string]*jwtauthnv3.JwtRequirement{
|
||||||
|
JwtAuthenticatedRequirement: {
|
||||||
|
RequiresType: &jwtauthnv3.JwtRequirement_ProviderName{
|
||||||
|
ProviderName: JwtProviderName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RBACPerRoute(cfg *rbacv3.RBAC) *rbacv3.RBACPerRoute {
|
||||||
|
return &rbacv3.RBACPerRoute{Rbac: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RBACAllowAllConfig() *rbacv3.RBAC {
|
||||||
|
return &rbacv3.RBAC{
|
||||||
|
Rules: &rbacv3cfg.RBAC{
|
||||||
|
Action: rbacv3cfg.RBAC_ALLOW,
|
||||||
|
Policies: map[string]*rbacv3cfg.Policy{
|
||||||
|
"Allow anyone": {
|
||||||
|
Permissions: []*rbacv3cfg.Permission{{
|
||||||
|
Rule: &rbacv3cfg.Permission_Any{Any: true},
|
||||||
|
}},
|
||||||
|
Principals: []*rbacv3cfg.Principal{{
|
||||||
|
Identifier: &rbacv3cfg.Principal_Any{
|
||||||
|
Any: true,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RBACRequireAuthConfig() *rbacv3.RBAC {
|
||||||
|
return &rbacv3.RBAC{
|
||||||
|
Rules: &rbacv3cfg.RBAC{
|
||||||
|
Action: rbacv3cfg.RBAC_ALLOW,
|
||||||
|
Policies: map[string]*rbacv3cfg.Policy{
|
||||||
|
"allow admin and anon roles": {
|
||||||
|
Permissions: []*rbacv3cfg.Permission{{
|
||||||
|
Rule: &rbacv3cfg.Permission_Any{Any: true},
|
||||||
|
}},
|
||||||
|
Principals: []*rbacv3cfg.Principal{{
|
||||||
|
Identifier: &rbacv3cfg.Principal_OrIds{
|
||||||
|
OrIds: &rbacv3cfg.Principal_Set{
|
||||||
|
Ids: []*rbacv3cfg.Principal{
|
||||||
|
{
|
||||||
|
Identifier: &rbacv3cfg.Principal_Metadata{
|
||||||
|
Metadata: &matcherv3.MetadataMatcher{
|
||||||
|
Filter: FilterNameJwtAuthn,
|
||||||
|
Path: []*matcherv3.MetadataMatcher_PathSegment{
|
||||||
|
{
|
||||||
|
Segment: &matcherv3.MetadataMatcher_PathSegment_Key{
|
||||||
|
Key: "jwt_payload",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Segment: &matcherv3.MetadataMatcher_PathSegment_Key{
|
||||||
|
Key: "role",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Value: &matcherv3.ValueMatcher{
|
||||||
|
MatchPattern: &matcherv3.ValueMatcher_OrMatch{
|
||||||
|
OrMatch: &matcherv3.OrMatcher{
|
||||||
|
ValueMatchers: []*matcherv3.ValueMatcher{
|
||||||
|
{
|
||||||
|
MatchPattern: &matcherv3.ValueMatcher_StringMatch{
|
||||||
|
StringMatch: &matcherv3.StringMatcher{
|
||||||
|
MatchPattern: &matcherv3.StringMatcher_Exact{
|
||||||
|
Exact: "anon",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MatchPattern: &matcherv3.ValueMatcher_StringMatch{
|
||||||
|
StringMatch: &matcherv3.StringMatcher{
|
||||||
|
MatchPattern: &matcherv3.StringMatcher_Exact{
|
||||||
|
Exact: "authenticated",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MatchPattern: &matcherv3.ValueMatcher_StringMatch{
|
||||||
|
StringMatch: &matcherv3.StringMatcher{
|
||||||
|
MatchPattern: &matcherv3.StringMatcher_Exact{
|
||||||
|
Exact: "admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
327
internal/controlplane/controller.go
Normal file
327
internal/controlplane/controller.go
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
|
listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||||
|
route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||||
|
router "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
|
||||||
|
hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||||
|
"github.com/envoyproxy/go-control-plane/pkg/cache/types"
|
||||||
|
"github.com/envoyproxy/go-control-plane/pkg/cache/v3"
|
||||||
|
"github.com/envoyproxy/go-control-plane/pkg/resource/v3"
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
discoveryv1 "k8s.io/api/discovery/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/selection"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnexpectedObject = errors.New("unexpected object")
|
||||||
|
ErrNoEnvoyClusterLabel = errors.New("no Envoy cluster label set")
|
||||||
|
|
||||||
|
supabaseServices = []string{
|
||||||
|
supabase.ServiceConfig.Postgrest.Name,
|
||||||
|
supabase.ServiceConfig.Auth.Name,
|
||||||
|
supabase.ServiceConfig.PGMeta.Name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type EndpointsController struct {
|
||||||
|
lock sync.Mutex
|
||||||
|
Client client.WithWatch
|
||||||
|
Cache cache.SnapshotCache
|
||||||
|
envoyClusters map[string]*envoyClusterServices
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *EndpointsController) Run(ctx context.Context) error {
|
||||||
|
var (
|
||||||
|
logger = ctrl.Log.WithName("endpoints-controller")
|
||||||
|
endpointSlices discoveryv1.EndpointSliceList
|
||||||
|
)
|
||||||
|
|
||||||
|
selector := labels.NewSelector()
|
||||||
|
|
||||||
|
partOfRequirement, err := labels.NewRequirement(meta.WellKnownLabel.PartOf, selection.Equals, []string{"supabase"})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("preparing watcher selectors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nameRequirement, err := labels.NewRequirement(meta.WellKnownLabel.Name, selection.In, supabaseServices)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("preparing watcher selectors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
envoyClusterRequirement, err := labels.NewRequirement(meta.SupabaseLabel.EnvoyCluster, selection.Exists, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("preparing watcher selectors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
selector.Add(*partOfRequirement, *nameRequirement, *envoyClusterRequirement)
|
||||||
|
|
||||||
|
watcher, err := c.Client.Watch(
|
||||||
|
ctx,
|
||||||
|
&endpointSlices,
|
||||||
|
client.MatchingLabelsSelector{
|
||||||
|
Selector: selector.Add(*partOfRequirement, *nameRequirement, *envoyClusterRequirement),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer watcher.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case ev, more := <-watcher.ResultChan():
|
||||||
|
if !more {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
eventLogger := logger.WithValues("event_type", ev.Type)
|
||||||
|
switch ev.Type {
|
||||||
|
case watch.Added, watch.Modified:
|
||||||
|
eps, ok := ev.Object.(*discoveryv1.EndpointSlice)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(fmt.Errorf("%w: %T", ErrUnexpectedObject, ev.Object), "expected EndpointSlice but got a different object type")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.handleModificationEvent(log.IntoContext(ctx, eventLogger), eps); err != nil {
|
||||||
|
logger.Error(err, "error occurred during event handling")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *EndpointsController) handleModificationEvent(ctx context.Context, epSlice *discoveryv1.EndpointSlice) error {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger = log.FromContext(ctx)
|
||||||
|
instanceKey string
|
||||||
|
svc *envoyClusterServices
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.Info("Observed endpoint slice", "name", epSlice.Name)
|
||||||
|
|
||||||
|
if c.envoyClusters == nil {
|
||||||
|
c.envoyClusters = make(map[string]*envoyClusterServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
envoyNodeName, ok := epSlice.Labels[meta.SupabaseLabel.EnvoyCluster]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%w: at object %s", ErrNoEnvoyClusterLabel, epSlice.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceKey = fmt.Sprintf("%s:%s", envoyNodeName, epSlice.Namespace)
|
||||||
|
|
||||||
|
if svc, ok = c.envoyClusters[instanceKey]; !ok {
|
||||||
|
svc = new(envoyClusterServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.UpsertEndpoints(epSlice)
|
||||||
|
|
||||||
|
c.envoyClusters[instanceKey] = svc
|
||||||
|
|
||||||
|
return c.updateSnapshot(ctx, instanceKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *EndpointsController) updateSnapshot(ctx context.Context, instance string) error {
|
||||||
|
latestVersion := strconv.FormatInt(time.Now().UTC().UnixMilli(), 10)
|
||||||
|
|
||||||
|
snapshot, err := c.envoyClusters[instance].snapshot(instance, latestVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Cache.SetSnapshot(ctx, instance, snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
type envoyClusterServices struct {
|
||||||
|
Postgrest *PostgrestCluster
|
||||||
|
GoTrue *GoTrueCluster
|
||||||
|
PGMeta *PGMetaCluster
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *envoyClusterServices) UpsertEndpoints(eps *discoveryv1.EndpointSlice) {
|
||||||
|
switch eps.Labels[meta.WellKnownLabel.Name] {
|
||||||
|
case supabase.ServiceConfig.Postgrest.Name:
|
||||||
|
if s.Postgrest == nil {
|
||||||
|
s.Postgrest = new(PostgrestCluster)
|
||||||
|
}
|
||||||
|
s.Postgrest.AddOrUpdateEndpoints(eps)
|
||||||
|
case supabase.ServiceConfig.Auth.Name:
|
||||||
|
if s.GoTrue == nil {
|
||||||
|
s.GoTrue = new(GoTrueCluster)
|
||||||
|
}
|
||||||
|
s.GoTrue.AddOrUpdateEndpoints(eps)
|
||||||
|
case supabase.ServiceConfig.PGMeta.Name:
|
||||||
|
if s.PGMeta == nil {
|
||||||
|
s.PGMeta = new(PGMetaCluster)
|
||||||
|
}
|
||||||
|
s.PGMeta.AddOrUpdateEndpoints(eps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapshot, error) {
|
||||||
|
const (
|
||||||
|
routeName = "supabase"
|
||||||
|
vHostName = "supabase"
|
||||||
|
listenerName = "supabase"
|
||||||
|
)
|
||||||
|
|
||||||
|
manager := &hcm.HttpConnectionManager{
|
||||||
|
CodecType: hcm.HttpConnectionManager_AUTO,
|
||||||
|
StatPrefix: "http",
|
||||||
|
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
|
||||||
|
Rds: &hcm.Rds{
|
||||||
|
ConfigSource: &corev3.ConfigSource{
|
||||||
|
ResourceApiVersion: resource.DefaultAPIVersion,
|
||||||
|
ConfigSourceSpecifier: &corev3.ConfigSource_ApiConfigSource{
|
||||||
|
ApiConfigSource: &corev3.ApiConfigSource{
|
||||||
|
TransportApiVersion: resource.DefaultAPIVersion,
|
||||||
|
ApiType: corev3.ApiConfigSource_GRPC,
|
||||||
|
SetNodeOnFirstMessageOnly: true,
|
||||||
|
GrpcServices: []*corev3.GrpcService{{
|
||||||
|
TargetSpecifier: &corev3.GrpcService_EnvoyGrpc_{
|
||||||
|
EnvoyGrpc: &corev3.GrpcService_EnvoyGrpc{ClusterName: "supabase-control-plane"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RouteConfigName: routeName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HttpFilters: []*hcm.HttpFilter{
|
||||||
|
{
|
||||||
|
Name: FilterNameJwtAuthn,
|
||||||
|
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(JWTFilterConfig())},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: FilterNameCORS,
|
||||||
|
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(Cors())},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: FilterNameHttpRouter,
|
||||||
|
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(new(router.Router))},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
routeCfg := &route.RouteConfiguration{
|
||||||
|
Name: routeName,
|
||||||
|
VirtualHosts: []*route.VirtualHost{{
|
||||||
|
Name: "supabase",
|
||||||
|
Domains: []string{"*"},
|
||||||
|
TypedPerFilterConfig: map[string]*anypb.Any{
|
||||||
|
FilterNameJwtAuthn: MustAny(JWTPerRouteConfig()),
|
||||||
|
FilterNameRBAC: MustAny(RBACPerRoute(RBACRequireAuthConfig())),
|
||||||
|
},
|
||||||
|
Routes: slices.Concat(
|
||||||
|
s.Postgrest.Routes(instance),
|
||||||
|
s.GoTrue.Routes(instance),
|
||||||
|
s.PGMeta.Routes(instance),
|
||||||
|
),
|
||||||
|
}},
|
||||||
|
TypedPerFilterConfig: map[string]*anypb.Any{
|
||||||
|
FilterNameCORS: MustAny(CorsPolicy()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
listener := &listener.Listener{
|
||||||
|
Name: listenerName,
|
||||||
|
Address: &corev3.Address{
|
||||||
|
Address: &corev3.Address_SocketAddress{
|
||||||
|
SocketAddress: &corev3.SocketAddress{
|
||||||
|
Protocol: corev3.SocketAddress_TCP,
|
||||||
|
Address: "0.0.0.0",
|
||||||
|
PortSpecifier: &corev3.SocketAddress_PortValue{
|
||||||
|
PortValue: 8000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FilterChains: []*listener.FilterChain{
|
||||||
|
{
|
||||||
|
Filters: []*listener.Filter{
|
||||||
|
{
|
||||||
|
Name: FilterNameHttpConnectionManager,
|
||||||
|
ConfigType: &listener.Filter_TypedConfig{
|
||||||
|
TypedConfig: MustAny(manager),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSnapshot := map[resource.Type][]types.Resource{
|
||||||
|
resource.ClusterType: castResources(
|
||||||
|
slices.Concat(
|
||||||
|
s.Postgrest.Cluster(instance),
|
||||||
|
s.GoTrue.Cluster(instance),
|
||||||
|
s.PGMeta.Cluster(instance),
|
||||||
|
)...),
|
||||||
|
resource.RouteType: {routeCfg},
|
||||||
|
resource.ListenerType: {listener},
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot, err := cache.NewSnapshot(
|
||||||
|
version,
|
||||||
|
rawSnapshot,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := snapshot.Consistent(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func castResources[T types.Resource](from ...T) []types.Resource {
|
||||||
|
result := make([]types.Resource, len(from))
|
||||||
|
for idx := range from {
|
||||||
|
result[idx] = from[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
32
internal/controlplane/cors.go
Normal file
32
internal/controlplane/cors.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
|
corsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3"
|
||||||
|
matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
|
||||||
|
typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Cors() *corsv3.Cors {
|
||||||
|
return new(corsv3.Cors)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CorsPolicy() *corsv3.CorsPolicy {
|
||||||
|
return &corsv3.CorsPolicy{
|
||||||
|
AllowMethods: "*",
|
||||||
|
AllowHeaders: "*",
|
||||||
|
AllowOriginStringMatch: []*matcherv3.StringMatcher{{
|
||||||
|
MatchPattern: &matcherv3.StringMatcher_SafeRegex{
|
||||||
|
SafeRegex: &matcherv3.RegexMatcher{
|
||||||
|
Regex: `\*`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
FilterEnabled: &corev3.RuntimeFractionalPercent{
|
||||||
|
DefaultValue: &typev3.FractionalPercent{
|
||||||
|
Numerator: 100,
|
||||||
|
Denominator: typev3.FractionalPercent_HUNDRED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
90
internal/controlplane/endpoints.go
Normal file
90
internal/controlplane/endpoints.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
package controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||||
|
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
|
endpointv3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
||||||
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
discoveryv1 "k8s.io/api/discovery/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceCluster struct {
|
||||||
|
ServiceEndpoints map[string]Endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServiceCluster) AddOrUpdateEndpoints(eps *discoveryv1.EndpointSlice) {
|
||||||
|
if c.ServiceEndpoints == nil {
|
||||||
|
c.ServiceEndpoints = make(map[string]Endpoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ServiceEndpoints[eps.Name] = newEndpointsFromSlice(eps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ServiceCluster) Cluster(name string, port uint32) *clusterv3.Cluster {
|
||||||
|
return &clusterv3.Cluster{
|
||||||
|
Name: name,
|
||||||
|
ConnectTimeout: durationpb.New(5 * time.Second),
|
||||||
|
ClusterDiscoveryType: &clusterv3.Cluster_Type{Type: clusterv3.Cluster_STATIC},
|
||||||
|
LbPolicy: clusterv3.Cluster_ROUND_ROBIN,
|
||||||
|
LoadAssignment: &endpointv3.ClusterLoadAssignment{
|
||||||
|
ClusterName: name,
|
||||||
|
Endpoints: c.endpoints(port),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ServiceCluster) endpoints(port uint32) []*endpointv3.LocalityLbEndpoints {
|
||||||
|
eps := make([]*endpointv3.LocalityLbEndpoints, 0, len(c.ServiceEndpoints))
|
||||||
|
|
||||||
|
for _, sep := range c.ServiceEndpoints {
|
||||||
|
eps = append(eps, &endpointv3.LocalityLbEndpoints{
|
||||||
|
LbEndpoints: sep.LBEndpoints(port),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return eps
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEndpointsFromSlice(eps *discoveryv1.EndpointSlice) Endpoints {
|
||||||
|
var result Endpoints
|
||||||
|
|
||||||
|
for _, ep := range eps.Endpoints {
|
||||||
|
if ep.Conditions.Ready != nil && *ep.Conditions.Ready {
|
||||||
|
result.Addresses = append(result.Addresses, ep.Addresses...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoints struct {
|
||||||
|
Addresses []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Endpoints) LBEndpoints(port uint32) []*endpointv3.LbEndpoint {
|
||||||
|
endpoints := make([]*endpointv3.LbEndpoint, 0, len(e.Addresses))
|
||||||
|
|
||||||
|
for _, ep := range e.Addresses {
|
||||||
|
endpoints = append(endpoints, &endpointv3.LbEndpoint{
|
||||||
|
HostIdentifier: &endpointv3.LbEndpoint_Endpoint{
|
||||||
|
Endpoint: &endpointv3.Endpoint{
|
||||||
|
Address: &corev3.Address{
|
||||||
|
Address: &corev3.Address_SocketAddress{
|
||||||
|
SocketAddress: &corev3.SocketAddress{
|
||||||
|
Address: ep,
|
||||||
|
Protocol: corev3.SocketAddress_TCP,
|
||||||
|
PortSpecifier: &corev3.SocketAddress_PortValue{
|
||||||
|
PortValue: port,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoints
|
||||||
|
}
|
9
internal/controlplane/filters.go
Normal file
9
internal/controlplane/filters.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package controlplane
|
||||||
|
|
||||||
|
const (
|
||||||
|
FilterNameJwtAuthn = "envoy.filters.http.jwt_authn"
|
||||||
|
FilterNameRBAC = "envoy.filters.http.rbac"
|
||||||
|
FilterNameCORS = "envoy.filters.http.cors"
|
||||||
|
FilterNameHttpRouter = "envoy.filters.http.router"
|
||||||
|
FilterNameHttpConnectionManager = "envoy.filters.network.http_connection_manager"
|
||||||
|
)
|
76
internal/controlplane/gotrue.go
Normal file
76
internal/controlplane/gotrue.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||||
|
routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||||
|
matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GoTrueCluster struct {
|
||||||
|
ServiceCluster
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GoTrueCluster) Cluster(instance string) []*clusterv3.Cluster {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*clusterv3.Cluster{c.ServiceCluster.Cluster(fmt.Sprintf("auth@%s", instance), 9999)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GoTrueCluster) Routes(instance string) []*routev3.Route {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*routev3.Route{
|
||||||
|
{
|
||||||
|
Name: "GoTrue (Open) /auth/v1/(callback|verify) -> http://auth:9999/$1",
|
||||||
|
Match: &routev3.RouteMatch{
|
||||||
|
PathSpecifier: &routev3.RouteMatch_SafeRegex{
|
||||||
|
SafeRegex: &matcherv3.RegexMatcher{
|
||||||
|
Regex: `/auth/v1/(callback|verify|authorize)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: &routev3.Route_Route{
|
||||||
|
Route: &routev3.RouteAction{
|
||||||
|
ClusterSpecifier: &routev3.RouteAction_Cluster{
|
||||||
|
Cluster: fmt.Sprintf("%s@%s", supabase.ServiceConfig.Auth.Name, instance),
|
||||||
|
},
|
||||||
|
RegexRewrite: &matcherv3.RegexMatchAndSubstitute{
|
||||||
|
Pattern: &matcherv3.RegexMatcher{
|
||||||
|
Regex: `/auth/v1/(callback|verify|authorize)`,
|
||||||
|
},
|
||||||
|
Substitution: `/\1`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TypedPerFilterConfig: map[string]*anypb.Any{
|
||||||
|
FilterNameRBAC: MustAny(RBACPerRoute(RBACAllowAllConfig())),
|
||||||
|
FilterNameJwtAuthn: MustAny(JWTAllowAll()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "GoTrue: /auth/v1/* -> http://auth:9999/*",
|
||||||
|
Match: &routev3.RouteMatch{
|
||||||
|
PathSpecifier: &routev3.RouteMatch_Prefix{
|
||||||
|
Prefix: "/auth/v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: &routev3.Route_Route{
|
||||||
|
Route: &routev3.RouteAction{
|
||||||
|
ClusterSpecifier: &routev3.RouteAction_Cluster{
|
||||||
|
Cluster: fmt.Sprintf("%s@%s", supabase.ServiceConfig.Auth.Name, instance),
|
||||||
|
},
|
||||||
|
PrefixRewrite: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
46
internal/controlplane/pg-meta.go
Normal file
46
internal/controlplane/pg-meta.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||||
|
routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PGMetaCluster struct {
|
||||||
|
ServiceCluster
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PGMetaCluster) Cluster(instance string) []*clusterv3.Cluster {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []*clusterv3.Cluster{
|
||||||
|
c.ServiceCluster.Cluster(fmt.Sprintf("%s@%s", supabase.ServiceConfig.PGMeta.Name, instance), uint32(supabase.ServiceConfig.PGMeta.Defaults.APIPort)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PGMetaCluster) Routes(instance string) []*routev3.Route {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*routev3.Route{{
|
||||||
|
Name: "pg-meta: /pg/* -> http://pg-meta:8080/*",
|
||||||
|
Match: &routev3.RouteMatch{
|
||||||
|
PathSpecifier: &routev3.RouteMatch_Prefix{
|
||||||
|
Prefix: "/pg/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: &routev3.Route_Route{
|
||||||
|
Route: &routev3.RouteAction{
|
||||||
|
ClusterSpecifier: &routev3.RouteAction_Cluster{
|
||||||
|
Cluster: fmt.Sprintf("%s@%s", supabase.ServiceConfig.PGMeta.Name, instance),
|
||||||
|
},
|
||||||
|
PrefixRewrite: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
71
internal/controlplane/postgrest.go
Normal file
71
internal/controlplane/postgrest.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||||
|
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
|
routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostgrestCluster struct {
|
||||||
|
ServiceCluster
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PostgrestCluster) Cluster(instance string) []*clusterv3.Cluster {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []*clusterv3.Cluster{
|
||||||
|
c.ServiceCluster.Cluster(fmt.Sprintf("%s@%s", supabase.ServiceConfig.Postgrest.Name, instance), 3000),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PostgrestCluster) Routes(instance string) []*routev3.Route {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*routev3.Route{
|
||||||
|
{
|
||||||
|
Name: "PostgREST: /rest/v1/* -> http://rest:3000/*",
|
||||||
|
Match: &routev3.RouteMatch{
|
||||||
|
PathSpecifier: &routev3.RouteMatch_Prefix{
|
||||||
|
Prefix: "/rest/v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: &routev3.Route_Route{
|
||||||
|
Route: &routev3.RouteAction{
|
||||||
|
ClusterSpecifier: &routev3.RouteAction_Cluster{
|
||||||
|
Cluster: fmt.Sprintf("%s@%s", supabase.ServiceConfig.Postgrest.Name, instance),
|
||||||
|
},
|
||||||
|
PrefixRewrite: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql",
|
||||||
|
Match: &routev3.RouteMatch{
|
||||||
|
PathSpecifier: &routev3.RouteMatch_Prefix{
|
||||||
|
Prefix: "/graphql/v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: &routev3.Route_Route{
|
||||||
|
Route: &routev3.RouteAction{
|
||||||
|
ClusterSpecifier: &routev3.RouteAction_Cluster{
|
||||||
|
Cluster: fmt.Sprintf("%s@%s", supabase.ServiceConfig.Postgrest.Name, instance),
|
||||||
|
},
|
||||||
|
PrefixRewrite: "/rpc/graphql",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequestHeadersToAdd: []*corev3.HeaderValueOption{{
|
||||||
|
Header: &corev3.HeaderValue{
|
||||||
|
Key: "Content-Profile",
|
||||||
|
Value: "graphql_public",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
15
internal/controlplane/utils.go
Normal file
15
internal/controlplane/utils.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
proto "google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MustAny(msg proto.Message) *anypb.Any {
|
||||||
|
a, err := anypb.New(msg)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
69
internal/db/roles_manager.go
Normal file
69
internal/db/roles_manager.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/assets/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
alterUserPwd = `ALTER ROLE %s WITH PASSWORD '%s';`
|
||||||
|
checkUserExists = `SELECT 1 FROM pg_user WHERE usename = $1;`
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRolesManager(conn *pgx.Conn) RolesManager {
|
||||||
|
return RolesManager{
|
||||||
|
Conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RolesManager struct {
|
||||||
|
Conn *pgx.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mgr RolesManager) UpdateRolePassword(ctx context.Context, roleName string, password []byte) error {
|
||||||
|
if err := mgr.ensureLoginRoleExists(ctx, roleName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mgr.Conn.Exec(ctx, fmt.Sprintf(alterUserPwd, roleName, password))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mgr RolesManager) ensureLoginRoleExists(ctx context.Context, roleName string) error {
|
||||||
|
logger := log.FromContext(ctx).WithValues("role_name", roleName)
|
||||||
|
|
||||||
|
rows, err := mgr.Conn.Query(ctx, checkUserExists, roleName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
_, err = pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (out int, err error) {
|
||||||
|
err = row.Scan(&out)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info("No rows, this means the role does not exists, creating it now")
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
script, err := migrations.RoleCreationScript(roleName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = mgr.Conn.Exec(ctx, script.Content)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
48
internal/jwk/key.go
Normal file
48
internal/jwk/key.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package jwk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeyType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeyTypeEC KeyType = "EC"
|
||||||
|
KeyTypeRSA KeyType = "RSA"
|
||||||
|
KeyTypeOctetSequence KeyType = "oct"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Algorithm string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AlgorithmNone Algorithm = ""
|
||||||
|
AlgorithmHS256 Algorithm = "HS256"
|
||||||
|
AlgorithmHS384 Algorithm = "HS384"
|
||||||
|
AlgorithmHS512 Algorithm = "HS512"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ json.Marshaler = (*SymmetricKey)(nil)
|
||||||
|
|
||||||
|
type SymmetricKey struct {
|
||||||
|
Algorithm Algorithm
|
||||||
|
Key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements json.Marshaler.
|
||||||
|
func (s SymmetricKey) MarshalJSON() ([]byte, error) {
|
||||||
|
if s.Algorithm == AlgorithmNone {
|
||||||
|
s.Algorithm = AlgorithmHS256
|
||||||
|
}
|
||||||
|
tmp := struct {
|
||||||
|
KeyType KeyType `json:"kty"`
|
||||||
|
Algorithm Algorithm `json:"alg"`
|
||||||
|
Key string `json:"k"`
|
||||||
|
}{
|
||||||
|
KeyType: KeyTypeOctetSequence,
|
||||||
|
Algorithm: s.Algorithm,
|
||||||
|
Key: base64.RawURLEncoding.EncodeToString(s.Key),
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(tmp)
|
||||||
|
}
|
9
internal/jwk/set.go
Normal file
9
internal/jwk/set.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package jwk
|
||||||
|
|
||||||
|
type Key interface {
|
||||||
|
SymmetricKey
|
||||||
|
}
|
||||||
|
|
||||||
|
type Set[T Key] struct {
|
||||||
|
Keys []T `json:"keys"`
|
||||||
|
}
|
47
internal/meta/labels.go
Normal file
47
internal/meta/labels.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 meta
|
||||||
|
|
||||||
|
import supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||||
|
|
||||||
|
const (
|
||||||
|
WellKnownMetaPrefix = "app.kubernetes.io/"
|
||||||
|
)
|
||||||
|
|
||||||
|
var WellKnownLabel = struct {
|
||||||
|
Name string
|
||||||
|
Instance string
|
||||||
|
PartOf string
|
||||||
|
Version string
|
||||||
|
Component string
|
||||||
|
ManagedBy string
|
||||||
|
}{
|
||||||
|
Name: WellKnownMetaPrefix + "name",
|
||||||
|
Instance: WellKnownMetaPrefix + "instance",
|
||||||
|
PartOf: WellKnownMetaPrefix + "part-of",
|
||||||
|
Version: WellKnownMetaPrefix + "version",
|
||||||
|
Component: WellKnownMetaPrefix + "component",
|
||||||
|
ManagedBy: WellKnownMetaPrefix + "managed-by",
|
||||||
|
}
|
||||||
|
|
||||||
|
var SupabaseLabel = struct {
|
||||||
|
Reload string
|
||||||
|
EnvoyCluster string
|
||||||
|
}{
|
||||||
|
Reload: supabasev1alpha1.GroupVersion.Group + "/reload",
|
||||||
|
EnvoyCluster: supabasev1alpha1.GroupVersion.Group + "/envoy-cluster",
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue