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
|
||||
# Ignore build and test binaries.
|
||||
bin/
|
||||
.idea
|
||||
.devcontainer
|
||||
.github
|
||||
.zed
|
||||
hack
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1 +1,4 @@
|
|||
bin/
|
||||
out/
|
||||
.idea/
|
||||
.venv/
|
||||
|
|
|
@ -28,6 +28,7 @@ linters:
|
|||
- gofmt
|
||||
- goimports
|
||||
- gosimple
|
||||
- godox
|
||||
- govet
|
||||
- ineffassign
|
||||
- lll
|
||||
|
|
27
Dockerfile
27
Dockerfile
|
@ -1,33 +1,38 @@
|
|||
# Build the manager binary
|
||||
FROM golang:1.22 AS builder
|
||||
FROM golang:1.23.4 AS builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /workspace
|
||||
# Copy the Go Modules manifests
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
|
||||
# cache deps before building and copying source so that we don't need to re-download as much
|
||||
# and so that source changes don't invalidate our downloaded layer
|
||||
RUN go mod download
|
||||
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 cmd/main.go cmd/main.go
|
||||
COPY api/ api/
|
||||
COPY internal/ internal/
|
||||
COPY [ "go.*", "./" ]
|
||||
COPY [ "api", "api" ]
|
||||
COPY [ "assets/migrations", "assets/migrations" ]
|
||||
COPY [ "cmd", "cmd" ]
|
||||
COPY [ "infrastructure", "infrastructure" ]
|
||||
COPY [ "internal", "internal" ]
|
||||
COPY [ "magefiles", "magefiles" ]
|
||||
COPY [ "tools", "tools" ]
|
||||
|
||||
# Build
|
||||
# the GOARCH has not a default value to allow the binary be built according to the host where the command
|
||||
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
|
||||
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
|
||||
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
|
||||
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
|
||||
# Refer to https://github.com/GoogleContainerTools/distroless for more details
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
WORKDIR /
|
||||
COPY --from=builder /workspace/manager .
|
||||
COPY --from=builder /workspace/supabase-operator .
|
||||
USER 65532:65532
|
||||
|
||||
ENTRYPOINT ["/manager"]
|
||||
ENTRYPOINT ["/supabase-operator"]
|
||||
|
|
26
PROJECT
26
PROJECT
|
@ -17,4 +17,30 @@ resources:
|
|||
kind: Core
|
||||
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: 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"
|
||||
|
|
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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/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 {
|
||||
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) {
|
||||
if d.DSN != nil {
|
||||
return *d.DSN, nil
|
||||
}
|
||||
|
||||
if d.DSNFrom == nil {
|
||||
if d.DSNSecretRef == nil {
|
||||
return "", errors.New("DSN not set")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
data, ok := secret.Data[d.DSNFrom.Key]
|
||||
data, ok := secret.Data[d.DSNSecretRef.Key]
|
||||
if !ok {
|
||||
return "", errors.New("key not found in secret")
|
||||
return "", fmt.Errorf("%w: %s", ErrNoSuchSecretValue, d.DSNSecretRef.Key)
|
||||
}
|
||||
|
||||
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.
|
||||
type CoreSpec struct {
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
|
||||
JWT *JwtSpec `json:"jwt,omitempty"`
|
||||
Database Database `json:"database,omitempty"`
|
||||
Postgrest PostgrestSpec `json:"postgrest,omitempty"`
|
||||
Auth *AuthSpec `json:"auth,omitempty"`
|
||||
}
|
||||
|
||||
type MigrationStatus map[string]int64
|
||||
|
@ -72,11 +392,26 @@ func (s MigrationStatus) Record(name string) {
|
|||
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.
|
||||
type CoreStatus struct {
|
||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
AppliedMigrations MigrationStatus `json:"appliedMigrations,omitempty"`
|
||||
Database DatabaseStatus `json:"database,omitempty"`
|
||||
Conditions []CoreCondition `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
@ -100,6 +435,12 @@ type CoreList struct {
|
|||
Items []Core `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&Core{}, &CoreList{})
|
||||
func (l CoreList) Iter() iter.Seq[*Core] {
|
||||
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
|
||||
|
||||
import (
|
||||
"k8s.io/api/core/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *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.
|
||||
func (in *Core) DeepCopyInto(out *Core) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
|
@ -51,6 +319,23 @@ func (in *Core) DeepCopyObject() runtime.Object {
|
|||
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.
|
||||
func (in *CoreList) DeepCopyInto(out *CoreList) {
|
||||
*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.
|
||||
func (in *CoreSpec) DeepCopyInto(out *CoreSpec) {
|
||||
*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.
|
||||
|
@ -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.
|
||||
func (in *CoreStatus) DeepCopyInto(out *CoreStatus) {
|
||||
*out = *in
|
||||
if in.AppliedMigrations != nil {
|
||||
in, out := &in.AppliedMigrations, &out.AppliedMigrations
|
||||
*out = make(map[string]uint64, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
in.Database.DeepCopyInto(&out.Database)
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]CoreCondition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,9 +418,144 @@ func (in *CoreStatus) DeepCopy() *CoreStatus {
|
|||
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.
|
||||
func (in *Database) DeepCopyInto(out *Database) {
|
||||
*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.
|
||||
|
@ -135,3 +567,399 @@ func (in *Database) DeepCopy() *Database {
|
|||
in.DeepCopyInto(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 (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"iter"
|
||||
"path"
|
||||
|
@ -25,8 +26,18 @@ func MigrationScripts() iter.Seq2[Script, error] {
|
|||
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] {
|
||||
return iter.Seq2[Script, error](func(yield func(Script, error) bool) {
|
||||
return func(yield func(Script, error) bool) {
|
||||
files, err := migrationsFS.ReadDir(dir)
|
||||
if err != nil {
|
||||
yield(Script{}, err)
|
||||
|
@ -58,5 +69,5 @@ func readScripts(dir string) iter.Seq2[Script, error] {
|
|||
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
|
||||
}
|
146
cmd/main.go
146
cmd/main.go
|
@ -17,26 +17,22 @@ limitations under the License.
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"context"
|
||||
"os"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
"github.com/alecthomas/kong"
|
||||
"go.uber.org/zap/zapcore"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||
"code.icb4dc0.de/prskr/supabase-operator/internal/controller"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
|
@ -52,117 +48,45 @@ func init() {
|
|||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
type app struct {
|
||||
Manager manager `cmd:"" name:"manager" help:"Run the Kubernetes operator"`
|
||||
ControlPlane controlPlane `cmd:"" name:"control-plane" help:"Run the Envoy control plane"`
|
||||
|
||||
Logging struct {
|
||||
Development bool `name:"development" default:"false"`
|
||||
Level zapcore.Level `name:"level" default:"info"`
|
||||
StacktraceLevel zapcore.Level `name:"stacktrace-level" default:"warn"`
|
||||
} `embed:"" prefix:"logging."`
|
||||
}
|
||||
|
||||
func (a app) AfterApply(kongctx *kong.Context) error {
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||
// Rapid Reset CVEs. For more information see:
|
||||
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||
disableHTTP2 := func(c *tls.Config) {
|
||||
setupLog.Info("disabling http/2")
|
||||
c.NextProtos = []string{"http/1.1"}
|
||||
Development: a.Logging.Development,
|
||||
Level: a.Logging.Level,
|
||||
StacktraceLevel: a.Logging.StacktraceLevel,
|
||||
TimeEncoder: zapcore.ISO8601TimeEncoder,
|
||||
}
|
||||
|
||||
if !enableHTTP2 {
|
||||
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||
logger := zap.New(zap.UseFlagOptions(&opts))
|
||||
ctrl.SetLogger(logger)
|
||||
kongctx.Bind(logger)
|
||||
|
||||
logger.Info("Completed logger setup")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
webhookServer := webhook.NewServer(webhook.Options{
|
||||
TLSOpts: tlsOpts,
|
||||
})
|
||||
func main() {
|
||||
var app app
|
||||
|
||||
// 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: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
TLSOpts: tlsOpts,
|
||||
}
|
||||
kongCtx := kong.Parse(
|
||||
&app,
|
||||
kong.Name("supabase-operator"),
|
||||
kong.BindTo(ctrl.SetupSignalHandler(), (*context.Context)(nil)),
|
||||
)
|
||||
|
||||
if secureMetrics {
|
||||
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||
// These configurations ensure that only authorized users and service accounts
|
||||
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||
// https://pkg.go.dev/sigs.k8s.io/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
|
||||
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||
// this setup is not recommended for production.
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||
Scheme: scheme,
|
||||
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")
|
||||
if err := kongCtx.Run(); err != nil {
|
||||
setupLog.Error(err, "failed to run app")
|
||||
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
|
@ -3,6 +3,8 @@
|
|||
# It should be run by config/default
|
||||
resources:
|
||||
- bases/supabase.k8s.icb4dc0.de_cores.yaml
|
||||
- bases/supabase.k8s.icb4dc0.de_apigateways.yaml
|
||||
- bases/supabase.k8s.icb4dc0.de_dashboards.yaml
|
||||
# +kubebuilder:scaffold:crdkustomizeresource
|
||||
|
||||
patches:
|
||||
|
@ -16,5 +18,5 @@ patches:
|
|||
|
||||
# [WEBHOOK] To enable webhook, uncomment the following section
|
||||
# the following config is for teaching kustomize how to do kustomization for CRDs.
|
||||
#configurations:
|
||||
#- kustomizeconfig.yaml
|
||||
configurations:
|
||||
- kustomizeconfig.yaml
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
# Adds namespace to all resources.
|
||||
namespace: supabase-operator-system
|
||||
namespace: supabase-system
|
||||
|
||||
# Value of this field is prepended to the
|
||||
# names of all resources, e.g. a deployment named
|
||||
# "wordpress" becomes "alices-wordpress".
|
||||
# Note that it should also match with the prefix (text before '-') of the namespace
|
||||
# field above.
|
||||
namePrefix: supabase-operator-
|
||||
namePrefix: supabase-
|
||||
|
||||
# Labels to add to all resources and selectors.
|
||||
#labels:
|
||||
|
@ -18,11 +18,9 @@ resources:
|
|||
- ../crd
|
||||
- ../rbac
|
||||
- ../manager
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
||||
# crd/kustomization.yaml
|
||||
#- ../webhook
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
|
||||
#- ../certmanager
|
||||
- ../control-plane
|
||||
- ../webhook
|
||||
- ../certmanager
|
||||
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
|
||||
#- ../prometheus
|
||||
# [METRICS] Expose the controller manager metrics service.
|
||||
|
@ -40,111 +38,111 @@ patches:
|
|||
- path: manager_metrics_patch.yaml
|
||||
target:
|
||||
kind: Deployment
|
||||
name: controller-manager
|
||||
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
||||
# crd/kustomization.yaml
|
||||
#- path: manager_webhook_patch.yaml
|
||||
- path: manager_webhook_patch.yaml
|
||||
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
|
||||
# Uncomment the following replacements to add the cert-manager CA injection annotations
|
||||
#replacements:
|
||||
# - source: # Uncomment the following block if you have any webhook
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: webhook-service
|
||||
# fieldPath: .metadata.name # Name of the service
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# fieldPaths:
|
||||
# - .spec.dnsNames.0
|
||||
# - .spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - source:
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: webhook-service
|
||||
# fieldPath: .metadata.namespace # Namespace of the service
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# fieldPaths:
|
||||
# - .spec.dnsNames.0
|
||||
# - .spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 1
|
||||
# create: true
|
||||
#
|
||||
# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # This name should match the one in certificate.yaml
|
||||
# fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: ValidatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - source:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # This name should match the one in certificate.yaml
|
||||
# fieldPath: .metadata.name
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: ValidatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 1
|
||||
# create: true
|
||||
#
|
||||
# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # This name should match the one in certificate.yaml
|
||||
# fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: MutatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - source:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # This name should match the one in certificate.yaml
|
||||
# fieldPath: .metadata.name
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: MutatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 1
|
||||
# create: true
|
||||
#
|
||||
replacements:
|
||||
- source: # Uncomment the following block if you have any webhook
|
||||
kind: Service
|
||||
version: v1
|
||||
name: webhook-service
|
||||
fieldPath: .metadata.name # Name of the service
|
||||
targets:
|
||||
- select:
|
||||
kind: Certificate
|
||||
group: cert-manager.io
|
||||
version: v1
|
||||
fieldPaths:
|
||||
- .spec.dnsNames.0
|
||||
- .spec.dnsNames.1
|
||||
options:
|
||||
delimiter: "."
|
||||
index: 0
|
||||
create: true
|
||||
- source:
|
||||
kind: Service
|
||||
version: v1
|
||||
name: webhook-service
|
||||
fieldPath: .metadata.namespace # Namespace of the service
|
||||
targets:
|
||||
- select:
|
||||
kind: Certificate
|
||||
group: cert-manager.io
|
||||
version: v1
|
||||
fieldPaths:
|
||||
- .spec.dnsNames.0
|
||||
- .spec.dnsNames.1
|
||||
options:
|
||||
delimiter: "."
|
||||
index: 1
|
||||
create: true
|
||||
|
||||
- source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
|
||||
kind: Certificate
|
||||
group: cert-manager.io
|
||||
version: v1
|
||||
name: serving-cert # This name should match the one in certificate.yaml
|
||||
fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||
targets:
|
||||
- select:
|
||||
kind: ValidatingWebhookConfiguration
|
||||
fieldPaths:
|
||||
- .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
options:
|
||||
delimiter: "/"
|
||||
index: 0
|
||||
create: true
|
||||
- source:
|
||||
kind: Certificate
|
||||
group: cert-manager.io
|
||||
version: v1
|
||||
name: serving-cert # This name should match the one in certificate.yaml
|
||||
fieldPath: .metadata.name
|
||||
targets:
|
||||
- select:
|
||||
kind: ValidatingWebhookConfiguration
|
||||
fieldPaths:
|
||||
- .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
options:
|
||||
delimiter: "/"
|
||||
index: 1
|
||||
create: true
|
||||
|
||||
- source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
|
||||
kind: Certificate
|
||||
group: cert-manager.io
|
||||
version: v1
|
||||
name: serving-cert # This name should match the one in certificate.yaml
|
||||
fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||
targets:
|
||||
- select:
|
||||
kind: MutatingWebhookConfiguration
|
||||
fieldPaths:
|
||||
- .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
options:
|
||||
delimiter: "/"
|
||||
index: 0
|
||||
create: true
|
||||
- source:
|
||||
kind: Certificate
|
||||
group: cert-manager.io
|
||||
version: v1
|
||||
name: serving-cert # This name should match the one in certificate.yaml
|
||||
fieldPath: .metadata.name
|
||||
targets:
|
||||
- select:
|
||||
kind: MutatingWebhookConfiguration
|
||||
fieldPaths:
|
||||
- .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
options:
|
||||
delimiter: "/"
|
||||
index: 1
|
||||
create: true
|
||||
# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
|
||||
- op: add
|
||||
path: /spec/template/spec/containers/0/args/0
|
||||
path: /spec/template/spec/containers/0/args/1
|
||||
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/managed-by: kustomize
|
||||
name: controller-manager-metrics-service
|
||||
namespace: system
|
||||
namespace: supabase-system
|
||||
spec:
|
||||
ports:
|
||||
- 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
|
||||
app.kubernetes.io/name: supabase-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: system
|
||||
name: supabase-system
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
namespace: supabase-system
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: supabase-operator
|
||||
|
@ -50,21 +50,20 @@ spec:
|
|||
# - 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
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- command:
|
||||
- /manager
|
||||
args:
|
||||
- args:
|
||||
- manager
|
||||
- --leader-elect
|
||||
- --health-probe-bind-address=:8081
|
||||
image: controller:latest
|
||||
image: supabase-operator:latest
|
||||
name: manager
|
||||
env:
|
||||
- name: CONTROLLER_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
|
|
|
@ -8,7 +8,7 @@ metadata:
|
|||
app.kubernetes.io/name: supabase-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: allow-metrics-traffic
|
||||
namespace: system
|
||||
namespace: supabase-system
|
||||
spec:
|
||||
podSelector:
|
||||
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:
|
||||
- allow-webhook-traffic.yaml
|
||||
- allow-metrics-traffic.yaml
|
||||
|
|
|
@ -7,7 +7,7 @@ metadata:
|
|||
app.kubernetes.io/name: supabase-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: controller-manager-metrics-monitor
|
||||
namespace: system
|
||||
namespace: supabase-system
|
||||
spec:
|
||||
endpoints:
|
||||
- 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
|
|
@ -9,6 +9,10 @@ resources:
|
|||
- role_binding.yaml
|
||||
- leader_election_role.yaml
|
||||
- leader_election_role_binding.yaml
|
||||
# RBAC role for the control plane
|
||||
- control-plane-service_account.yaml
|
||||
- control-plane-role.yaml
|
||||
- control-plane-role_binding.yaml
|
||||
# The following RBAC configurations are used to protect
|
||||
# the metrics endpoint with authn/authz. These configurations
|
||||
# ensure that only authorized users and service accounts
|
||||
|
@ -22,6 +26,13 @@ resources:
|
|||
# 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
|
||||
|
|
|
@ -12,4 +12,4 @@ roleRef:
|
|||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
namespace: supabase-system
|
||||
|
|
|
@ -9,4 +9,4 @@ roleRef:
|
|||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
namespace: supabase-system
|
||||
|
|
|
@ -5,9 +5,29 @@ metadata:
|
|||
name: manager-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- supabase.k8s.icb4dc0.de
|
||||
- ""
|
||||
resources:
|
||||
- cores
|
||||
- configmaps
|
||||
- secrets
|
||||
- services
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
|
@ -19,13 +39,31 @@ rules:
|
|||
- apiGroups:
|
||||
- supabase.k8s.icb4dc0.de
|
||||
resources:
|
||||
- apigateways
|
||||
- cores
|
||||
- dashboards
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- supabase.k8s.icb4dc0.de
|
||||
resources:
|
||||
- apigateways/finalizers
|
||||
- cores/finalizers
|
||||
- dashboards/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- supabase.k8s.icb4dc0.de
|
||||
resources:
|
||||
- apigateways/status
|
||||
- cores/status
|
||||
- dashboards/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
|
|
|
@ -12,4 +12,4 @@ roleRef:
|
|||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
namespace: supabase-system
|
||||
|
|
|
@ -5,4 +5,4 @@ metadata:
|
|||
app.kubernetes.io/name: supabase-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
namespace: supabase-system
|
||||
|
|
|
@ -40,7 +40,7 @@ metadata:
|
|||
name: cluster-example
|
||||
spec:
|
||||
instances: 1
|
||||
imageName: ghcr.io/supabase/postgres:15.6.1.145
|
||||
imageName: ghcr.io/supabase/postgres:15.8.1.021
|
||||
postgresUID: 105
|
||||
postgresGID: 106
|
||||
|
|
@ -1,4 +1,11 @@
|
|||
## Append samples of your project ##
|
||||
|
||||
namespace: supabase-demo
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- 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
|
||||
kind: Core
|
||||
metadata:
|
||||
|
@ -8,5 +15,11 @@ metadata:
|
|||
spec:
|
||||
database:
|
||||
dsnFrom:
|
||||
name: example-cluster-credentials
|
||||
name: supabase-demo-credentials
|
||||
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
|
||||
|
||||
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/lestrrat-go/jwx/v2 v2.1.3
|
||||
github.com/magefile/mage v1.15.0
|
||||
github.com/onsi/ginkgo/v2 v2.19.0
|
||||
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
|
||||
k8s.io/api v0.31.0
|
||||
k8s.io/apimachinery v0.31.0
|
||||
|
@ -16,14 +21,19 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.15.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.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/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // 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/envoyproxy/protoc-gen-validate v1.1.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // 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/swag v0.22.4 // 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/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // 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/josharian/intern v1.0.0 // 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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // 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/pflag v1.0.5 // 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/proto/otlp v1.3.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.26.0 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/crypto v0.29.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/term v0.24.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/term v0.26.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // 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/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/yaml.v2 v2.4.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/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
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/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/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/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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/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/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/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
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-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/goccy/go-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/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
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/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/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/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
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/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
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/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
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/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.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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
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.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
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/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
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-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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
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-20190412213103-97732733099d/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.25.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.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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() {
|
||||
By("Reconciling the created resource")
|
||||
controllerReconciler := &CoreReconciler{
|
||||
controllerReconciler := &CoreDbReconciler{
|
||||
Client: k8sClient,
|
||||
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