feat(storage): prepare custom resource for storage API
Some checks failed
Deploy pages / deploy (push) Successful in 40s
release / release (push) Waiting to run
E2E Tests / Run on Ubuntu (push) Failing after 1m47s
Lint / Run on Ubuntu (push) Failing after 3m33s
Tests / Run on Ubuntu (push) Failing after 5m3s

This commit is contained in:
Peter 2025-01-21 21:54:53 +01:00
parent d02e2d4653
commit b55afea477
Signed by: prskr
GPG key ID: F56BED6903BC5E37
34 changed files with 1110 additions and 369 deletions

View file

@ -56,4 +56,8 @@ resources:
kind: Storage
path: code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1
version: v1alpha1
webhooks:
defaulting: true
validation: true
webhookVersion: v1
version: "3"

View file

@ -78,3 +78,11 @@ k8s_resource(
'supabase-controller-manager'
],
)
k8s_resource(
objects=["core-sample:Storage:supabase-demo"],
new_name='Storage',
resource_deps=[
'supabase-controller-manager'
],
)

View file

@ -22,6 +22,50 @@ import (
corev1 "k8s.io/api/core/v1"
)
type JwtSpec struct {
// SecretRef - object reference to the Secret where JWT values are stored
SecretName string `json:"secretName,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"`
}
func (s JwtSpec) SecretKeySelector() *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: s.SecretName,
},
Key: s.SecretKey,
}
}
func (s JwtSpec) AnonKeySelector() *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: s.SecretName,
},
Key: s.AnonKey,
}
}
func (s JwtSpec) ServiceKeySelector() *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: s.SecretName,
},
Key: s.ServiceKey,
}
}
type ImageSpec struct {
Image string `json:"image,omitempty"`
PullPolicy corev1.PullPolicy `json:"pullPolicy,omitempty"`

View file

@ -41,11 +41,11 @@ func init() {
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"`
Admin string `json:"supabaseAdmin,omitempty"`
Authenticator string `json:"authenticator,omitempty"`
AuthAdmin string `json:"supabaseAuthAdmin,omitempty"`
FunctionsAdmin string `json:"supabaseFunctionsAdmin,omitempty"`
StorageAdmin string `json:"supabaseStorageAdmin,omitempty"`
}
type DatabaseRoles struct {
@ -91,23 +91,10 @@ func (d Database) DSNEnv(key string) corev1.EnvVar {
}
type CoreJwtSpec struct {
JwtSpec `json:",inline"`
// 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"`
@ -115,7 +102,7 @@ type CoreJwtSpec struct {
func (s CoreJwtSpec) 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 {
if err := client.Get(ctx, types.NamespacedName{Name: s.SecretName}, &secret); err != nil {
return nil, nil
}
@ -129,15 +116,19 @@ func (s CoreJwtSpec) GetJWTSecret(ctx context.Context, client client.Client) ([]
func (s CoreJwtSpec) SecretKeySelector() *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: *s.SecretRef,
Key: s.SecretKey,
LocalObjectReference: corev1.LocalObjectReference{
Name: s.SecretName,
},
Key: s.SecretKey,
}
}
func (s CoreJwtSpec) JwksKeySelector() *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: *s.SecretRef,
Key: s.JwksKey,
LocalObjectReference: corev1.LocalObjectReference{
Name: s.SecretName,
},
Key: s.JwksKey,
}
}
@ -146,8 +137,10 @@ func (s CoreJwtSpec) SecretAsEnv(key string) corev1.EnvVar {
Name: key,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: *s.SecretRef,
Key: s.SecretKey,
LocalObjectReference: corev1.LocalObjectReference{
Name: s.SecretName,
},
Key: s.SecretKey,
},
},
}
@ -194,11 +187,21 @@ func (p *AuthProviderMeta) Vars(provider string) []corev1.EnvVar {
}}
}
type SmtpCredentialsReference struct {
SecretName string `json:"secretName"`
// UsernameKey
// +kubebuilder:default="username"
UsernameKey string `json:"usernameKey"`
// PasswordKey
// +kubebuilder:default="password"
PasswordKey string `json:"passwordKey"`
}
type EmailAuthSmtpSpec struct {
Host string `json:"host"`
Port uint16 `json:"port"`
MaxFrequency *uint `json:"maxFrequency,omitempty"`
CredentialsFrom *corev1.LocalObjectReference `json:"credentialsFrom"`
Host string `json:"host"`
Port uint16 `json:"port"`
MaxFrequency *uint `json:"maxFrequency,omitempty"`
CredentialsRef *SmtpCredentialsReference `json:"credentialsRef"`
}
type EmailAuthProvider struct {
@ -225,8 +228,10 @@ func (p *EmailAuthProvider) Vars(apiExternalURL string) []corev1.EnvVar {
Name: "GOTRUE_SMTP_USER",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: *p.SmtpSpec.CredentialsFrom,
Key: corev1.BasicAuthUsernameKey,
LocalObjectReference: corev1.LocalObjectReference{
Name: p.SmtpSpec.CredentialsRef.SecretName,
},
Key: p.SmtpSpec.CredentialsRef.UsernameKey,
},
},
},
@ -234,8 +239,10 @@ func (p *EmailAuthProvider) Vars(apiExternalURL string) []corev1.EnvVar {
Name: "GOTRUE_SMTP_PASS",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: *p.SmtpSpec.CredentialsFrom,
Key: corev1.BasicAuthPasswordKey,
LocalObjectReference: corev1.LocalObjectReference{
Name: p.SmtpSpec.CredentialsRef.SecretName,
},
Key: p.SmtpSpec.CredentialsRef.PasswordKey,
},
},
},

View file

@ -21,43 +21,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type DashboardJwtSpec struct {
// 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"`
// 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"`
}
func (s DashboardJwtSpec) SecretKeySelector() *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: *s.SecretRef,
Key: s.SecretKey,
}
}
func (s DashboardJwtSpec) AnonKeySelector() *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: *s.SecretRef,
Key: s.AnonKey,
}
}
func (s DashboardJwtSpec) ServiceKeySelector() *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: *s.SecretRef,
Key: s.ServiceKey,
}
}
type StudioSpec struct {
JWT *DashboardJwtSpec `json:"jwt,omitempty"`
JWT *JwtSpec `json:"jwt,omitempty"`
// WorkloadTemplate - customize the studio deployment
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"`
// GatewayServiceSelector - selector to find the service for the API gateway
@ -75,6 +40,16 @@ type PGMetaSpec struct {
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"`
}
type DbCredentialsReference struct {
SecretName string `json:"secretName"`
// UsernameKey
// +kubebuilder:default="username"
UsernameKey string `json:"usernameKey,omitempty"`
// PasswordKey
// +kubebuilder:default="password"
PasswordKey string `json:"passwordKey,omitempty"`
}
type DashboardDbSpec struct {
Host string `json:"host"`
// Port - Database port, typically 5432
@ -83,20 +58,24 @@ type DashboardDbSpec struct {
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"`
DBCredentialsRef *DbCredentialsReference `json:"dbCredentialsRef"`
}
func (s DashboardDbSpec) UserRef() *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: *s.DBCredentialsRef,
Key: corev1.BasicAuthUsernameKey,
LocalObjectReference: corev1.LocalObjectReference{
Name: s.DBCredentialsRef.SecretName,
},
Key: s.DBCredentialsRef.UsernameKey,
}
}
func (s DashboardDbSpec) PasswordRef() *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: *s.DBCredentialsRef,
Key: corev1.BasicAuthPasswordKey,
LocalObjectReference: corev1.LocalObjectReference{
Name: s.DBCredentialsRef.SecretName,
},
Key: s.DBCredentialsRef.PasswordKey,
}
}

View file

@ -17,26 +17,93 @@ limitations under the License.
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
type StorageBackend string
const (
StorageBackendFile StorageBackend = "file"
StorageBackendS3 StorageBackend = "s3"
)
type StorageApiDbSpec 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 *DbCredentialsReference `json:"dbCredentialsRef"`
}
func (s StorageApiDbSpec) UserRef() *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: s.DBCredentialsRef.SecretName,
},
Key: s.DBCredentialsRef.UsernameKey,
}
}
func (s StorageApiDbSpec) PasswordRef() *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: s.DBCredentialsRef.SecretName,
},
Key: s.DBCredentialsRef.PasswordKey,
}
}
type S3CredentialsRef struct {
SecretName string `json:"secretName"`
// AccessKeyIdKey - key in Secret where access key id will be referenced from
// +kubebuilder:default="accessKeyId"
AccessKeyIdKey string `json:"accessKeyIdKey,omitempty"`
// AccessSecretKeyKey - key in Secret where access secret key will be referenced from
// +kubebuilder:default="secretAccessKey"
AccessSecretKeyKey string `json:"accessSecretKeyKey,omitempty"`
}
type S3ProtocolSpec struct {
// Region - S3 region to use in the API
// +kubebuilder:default="us-east-1"
Region string `json:"region,omitempty"`
// AllowForwardedHeader
// +kubebuilder:default=true
AllowForwardedHeader bool `json:"allowForwardedHeader,omitempty"`
// CredentialsSecretRef - reference to the Secret where access key id and access secret key are stored
CredentialsSecretRef *S3CredentialsRef `json:"credentialsSecretRef,omitempty"`
}
// StorageSpec defines the desired state of Storage.
type StorageSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// Foo is an example field of Storage. Edit storage_types.go to remove/update
Foo string `json:"foo,omitempty"`
// BackendType - backend storage type to use
// +kubebuilder:validation:Enum={s3,file}
BackendType StorageBackend `json:"backendType"`
// FileSizeLimit - maximum file upload size in bytes
// +kubebuilder:default=52428800
FileSizeLimit uint64 `json:"fileSizeLimit,omitempty"`
// JwtAuth - Configure the JWT authentication parameters.
// This includes where to retrieve anon and service key from as well as JWT secret and JWKS references
// needed to validate JWTs send to the API
JwtAuth JwtSpec `json:"jwtAuth"`
// DBSpec - Configure access to the Postgres database
// In most cases this will reference the supabase-storage-admin credentials secret provided by the Core resource
DBSpec StorageApiDbSpec `json:"db"`
// S3 - Configure S3 protocol
S3 *S3ProtocolSpec `json:"s3,omitempty"`
// EnableImageTransformation - whether to deploy the image proxy
// the image proxy scale images to lower resolutions on demand to reduce traffic for instance for mobile devices
EnableImageTransformation bool `json:"enableImageTransformation,omitempty"`
}
// StorageStatus defines the observed state of Storage.
type StorageStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
type StorageStatus struct{}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

View file

@ -21,7 +21,7 @@ limitations under the License.
package v1alpha1
import (
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@ -345,16 +345,12 @@ func (in *Core) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CoreJwtSpec) DeepCopyInto(out *CoreJwtSpec) {
*out = *in
out.JwtSpec = in.JwtSpec
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 CoreJwtSpec.
@ -474,7 +470,7 @@ func (in *DashboardDbSpec) DeepCopyInto(out *DashboardDbSpec) {
*out = *in
if in.DBCredentialsRef != nil {
in, out := &in.DBCredentialsRef, &out.DBCredentialsRef
*out = new(v1.LocalObjectReference)
*out = new(DbCredentialsReference)
**out = **in
}
}
@ -489,26 +485,6 @@ func (in *DashboardDbSpec) DeepCopy() *DashboardDbSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardJwtSpec) DeepCopyInto(out *DashboardJwtSpec) {
*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 DashboardJwtSpec.
func (in *DashboardJwtSpec) DeepCopy() *DashboardJwtSpec {
if in == nil {
return nil
}
out := new(DashboardJwtSpec)
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
@ -599,7 +575,7 @@ func (in *Database) DeepCopyInto(out *Database) {
*out = new(v1.SecretKeySelector)
(*in).DeepCopyInto(*out)
}
in.Roles.DeepCopyInto(&out.Roles)
out.Roles = in.Roles
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Database.
@ -615,7 +591,7 @@ func (in *Database) DeepCopy() *Database {
// 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)
out.Secrets = in.Secrets
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRoles.
@ -631,31 +607,6 @@ func (in *DatabaseRoles) DeepCopy() *DatabaseRoles {
// 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.
@ -706,6 +657,21 @@ func (in *DatabaseStatus) DeepCopy() *DatabaseStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DbCredentialsReference) DeepCopyInto(out *DbCredentialsReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DbCredentialsReference.
func (in *DbCredentialsReference) DeepCopy() *DbCredentialsReference {
if in == nil {
return nil
}
out := new(DbCredentialsReference)
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
@ -745,9 +711,9 @@ func (in *EmailAuthSmtpSpec) DeepCopyInto(out *EmailAuthSmtpSpec) {
*out = new(uint)
**out = **in
}
if in.CredentialsFrom != nil {
in, out := &in.CredentialsFrom, &out.CredentialsFrom
*out = new(v1.LocalObjectReference)
if in.CredentialsRef != nil {
in, out := &in.CredentialsRef, &out.CredentialsRef
*out = new(SmtpCredentialsReference)
**out = **in
}
}
@ -839,6 +805,21 @@ func (in *ImageSpec) DeepCopy() *ImageSpec {
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
}
// 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) {
{
@ -946,12 +927,62 @@ func (in *PostgrestSpec) DeepCopy() *PostgrestSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *S3CredentialsRef) DeepCopyInto(out *S3CredentialsRef) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3CredentialsRef.
func (in *S3CredentialsRef) DeepCopy() *S3CredentialsRef {
if in == nil {
return nil
}
out := new(S3CredentialsRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *S3ProtocolSpec) DeepCopyInto(out *S3ProtocolSpec) {
*out = *in
if in.CredentialsSecretRef != nil {
in, out := &in.CredentialsSecretRef, &out.CredentialsSecretRef
*out = new(S3CredentialsRef)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3ProtocolSpec.
func (in *S3ProtocolSpec) DeepCopy() *S3ProtocolSpec {
if in == nil {
return nil
}
out := new(S3ProtocolSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SmtpCredentialsReference) DeepCopyInto(out *SmtpCredentialsReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SmtpCredentialsReference.
func (in *SmtpCredentialsReference) DeepCopy() *SmtpCredentialsReference {
if in == nil {
return nil
}
out := new(SmtpCredentialsReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Storage) DeepCopyInto(out *Storage) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
}
@ -973,6 +1004,26 @@ func (in *Storage) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StorageApiDbSpec) DeepCopyInto(out *StorageApiDbSpec) {
*out = *in
if in.DBCredentialsRef != nil {
in, out := &in.DBCredentialsRef, &out.DBCredentialsRef
*out = new(DbCredentialsReference)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageApiDbSpec.
func (in *StorageApiDbSpec) DeepCopy() *StorageApiDbSpec {
if in == nil {
return nil
}
out := new(StorageApiDbSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StorageList) DeepCopyInto(out *StorageList) {
*out = *in
@ -1008,6 +1059,13 @@ func (in *StorageList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StorageSpec) DeepCopyInto(out *StorageSpec) {
*out = *in
out.JwtAuth = in.JwtAuth
in.DBSpec.DeepCopyInto(&out.DBSpec)
if in.S3 != nil {
in, out := &in.S3, &out.S3
*out = new(S3ProtocolSpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageSpec.
@ -1040,8 +1098,8 @@ func (in *StudioSpec) DeepCopyInto(out *StudioSpec) {
*out = *in
if in.JWT != nil {
in, out := &in.JWT, &out.JWT
*out = new(DashboardJwtSpec)
(*in).DeepCopyInto(*out)
*out = new(JwtSpec)
**out = **in
}
if in.WorkloadTemplate != nil {
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate

View file

@ -156,13 +156,16 @@ func (m manager) Run(ctx context.Context) error {
}
if err = webhooksupabasev1alpha1.SetupAPIGatewayWebhookWithManager(mgr, webhookConfig); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "APIGateway")
os.Exit(1)
return fmt.Errorf("unable to create webhook: %w", err)
}
if err = webhooksupabasev1alpha1.SetupDashboardWebhookWithManager(mgr); err != nil {
return fmt.Errorf("unable to create webhook: %w", err)
}
if err = webhooksupabasev1alpha1.SetupStorageWebhookWithManager(mgr); err != nil {
return fmt.Errorf("unable to create webhook: %w", err)
}
}
// +kubebuilder:scaffold:builder

View file

@ -0,0 +1,20 @@
# The following manifests contain a self-signed issuer CR and a metrics certificate CR.
# More document can be found at https://docs.cert-manager.io
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: metrics-certs # this name should match the one appeared in kustomizeconfig.yaml
namespace: system
spec:
dnsNames:
# SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
# replacements in the config/default/kustomization.yaml file.
- SERVICE_NAME.SERVICE_NAMESPACE.svc
- SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local
issuerRef:
kind: Issuer
name: selfsigned-issuer
secretName: metrics-server-cert

View file

@ -0,0 +1,20 @@
# The following manifests contain a self-signed issuer CR and a certificate CR.
# More document can be found at https://docs.cert-manager.io
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml
namespace: system
spec:
# SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
# replacements in the config/default/kustomization.yaml file.
dnsNames:
- SERVICE_NAME.SERVICE_NAMESPACE.svc
- SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local
issuerRef:
kind: Issuer
name: selfsigned-issuer
secretName: webhook-server-cert

View file

@ -0,0 +1,13 @@
# The following manifest contains a self-signed issuer CR.
# More information 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: system
spec:
selfSigned: {}

View file

@ -105,22 +105,23 @@ spec:
type: string
smtpSpec:
properties:
credentialsFrom:
description: |-
LocalObjectReference contains enough information to let you locate the
referenced object inside the same namespace.
credentialsRef:
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
passwordKey:
default: password
description: PasswordKey
type: string
secretName:
type: string
usernameKey:
default: username
description: UsernameKey
type: string
required:
- passwordKey
- secretName
- usernameKey
type: object
x-kubernetes-map-type: atomic
host:
type: string
maxFrequency:
@ -128,7 +129,7 @@ spec:
port:
type: integer
required:
- credentialsFrom
- credentialsRef
- host
- port
type: object
@ -935,85 +936,15 @@ spec:
role that Supabase needs
properties:
authenticator:
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: string
supabaseAdmin:
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: string
supabaseAuthAdmin:
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: string
supabaseFunctionsAdmin:
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: string
supabaseStorageAdmin:
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: string
type: object
selfManaged:
description: |-
@ -1053,21 +984,10 @@ spec:
description: SecretKey - key in secret where to read the JWT HMAC
secret from
type: string
secretRef:
secretName:
description: SecretRef - object reference to the Secret where
JWT values are stored
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: string
serviceKey:
default: service_key
description: ServiceKey - key in secret where to read the service

View file

@ -46,17 +46,19 @@ spec:
DBCredentialsRef - reference to a Secret key where the DB credentials can be retrieved from
Credentials need to be stored in basic auth form
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
passwordKey:
default: password
description: PasswordKey
type: string
secretName:
type: string
usernameKey:
default: username
description: UsernameKey
type: string
required:
- secretName
type: object
x-kubernetes-map-type: atomic
dbName:
type: string
host:
@ -814,26 +816,20 @@ spec:
description: AnonKey - key in secret where to read the anon
JWT from
type: string
jwksKey:
default: jwks.json
description: JwksKey - key in secret where to read the JWKS
from
type: string
secretKey:
default: secret
description: SecretKey - key in secret where to read the JWT
HMAC secret from
type: string
secretRef:
secretName:
description: SecretRef - object reference to the Secret where
JWT values are stored
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: string
serviceKey:
default: service_key
description: ServiceKey - key in secret where to read the

View file

@ -39,10 +39,123 @@ spec:
spec:
description: StorageSpec defines the desired state of Storage.
properties:
foo:
description: Foo is an example field of Storage. Edit storage_types.go
to remove/update
backendType:
description: BackendType - backend storage type to use
enum:
- s3
- file
type: string
db:
description: |-
DBSpec - Configure access to the Postgres database
In most cases this will reference the supabase-storage-admin credentials secret provided by the Core resource
properties:
dbCredentialsRef:
description: |-
DBCredentialsRef - reference to a Secret key where the DB credentials can be retrieved from
Credentials need to be stored in basic auth form
properties:
passwordKey:
default: password
description: PasswordKey
type: string
secretName:
type: string
usernameKey:
default: username
description: UsernameKey
type: string
required:
- secretName
type: object
dbName:
type: string
host:
type: string
port:
default: 5432
description: Port - Database port, typically 5432
type: integer
required:
- dbCredentialsRef
- dbName
- host
type: object
enableImageTransformation:
description: |-
EnableImageTransformation - whether to deploy the image proxy
the image proxy scale images to lower resolutions on demand to reduce traffic for instance for mobile devices
type: boolean
fileSizeLimit:
default: 52428800
description: FileSizeLimit - maximum file upload size in bytes
format: int64
type: integer
jwtAuth:
description: |-
JwtAuth - Configure the JWT authentication parameters.
This includes where to retrieve anon and service key from as well as JWT secret and JWKS references
needed to validate JWTs send to the API
properties:
anonKey:
default: anon_key
description: AnonKey - key in secret where to read the anon JWT
from
type: string
jwksKey:
default: jwks.json
description: JwksKey - key in secret where to read the JWKS from
type: string
secretKey:
default: secret
description: SecretKey - key in secret where to read the JWT HMAC
secret from
type: string
secretName:
description: SecretRef - object reference to the Secret where
JWT values are stored
type: string
serviceKey:
default: service_key
description: ServiceKey - key in secret where to read the service
JWT from
type: string
type: object
s3:
description: S3 - Configure S3 protocol
properties:
allowForwardedHeader:
default: true
description: AllowForwardedHeader
type: boolean
credentialsSecretRef:
description: CredentialsSecretRef - reference to the Secret where
access key id and access secret key are stored
properties:
accessKeyIdKey:
default: accessKeyId
description: AccessKeyIdKey - key in Secret where access key
id will be referenced from
type: string
accessSecretKeyKey:
default: secretAccessKey
description: AccessSecretKeyKey - key in Secret where access
secret key will be referenced from
type: string
secretName:
type: string
required:
- secretName
type: object
region:
default: us-east-1
description: Region - S3 region to use in the API
type: string
type: object
required:
- backendType
- db
- jwtAuth
type: object
status:
description: StorageStatus defines the observed state of Storage.

View file

@ -10,12 +10,11 @@ spec:
host: cluster-example-rw.supabase-demo.svc
dbName: app
dbCredentialsRef:
name: db-roles-creds-supabase-admin
secretName: core-sample-db-creds-supabase-admin
studio:
externalUrl: http://localhost:8000
jwt:
anonKey: anon_key
secretKey: secret
secretRef:
name: core-sample-jwt
secretName: core-sample-jwt
serviceKey: service_key

View file

@ -4,6 +4,14 @@ metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: storage-sample
name: core-sample
spec:
# TODO(user): Add fields here
backendType: file
db:
host: cluster-example-rw.supabase-demo.svc
dbName: app
dbCredentialsRef:
secretName: core-sample-db-creds-supabase-storage-admin
enableImageTransformation: true
jwtAuth:
secretName: core-sample-jwt

View file

@ -64,6 +64,26 @@ webhooks:
resources:
- dashboards
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /mutate-supabase-k8s-icb4dc0-de-v1alpha1-storage
failurePolicy: Fail
name: mstorage-v1alpha1.kb.io
rules:
- apiGroups:
- supabase.k8s.icb4dc0.de
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- storages
sideEffects: None
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
@ -130,3 +150,23 @@ webhooks:
resources:
- dashboards
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-supabase-k8s-icb4dc0-de-v1alpha1-storage
failurePolicy: Fail
name: vstorage-v1alpha1.kb.io
rules:
- apiGroups:
- supabase.k8s.icb4dc0.de
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- storages
sideEffects: None

View file

@ -229,12 +229,12 @@ _Appears in:_
| 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 | | |
| `secretName` _string_ | 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 | |
| `secret` _string_ | Secret - JWT HMAC secret in plain text<br />This is WRITE-ONLY and will be copied to the SecretRef by the defaulter | | |
| `expiry` _integer_ | Expiry - expiration time in seconds for JWTs | 3600 | |
@ -314,26 +314,7 @@ _Appears in:_
| `host` _string_ | | | |
| `port` _integer_ | Port - Database port, typically 5432 | 5432 | |
| `dbName` _string_ | | | |
| `dbCredentialsRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | DBCredentialsRef - reference to a Secret key where the DB credentials can be retrieved from<br />Credentials need to be stored in basic auth form | | |
#### DashboardJwtSpec
_Appears in:_
- [StudioSpec](#studiospec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `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 | |
| `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 | |
| `dbCredentialsRef` _[DbCredentialsReference](#dbcredentialsreference)_ | DBCredentialsRef - reference to a Secret key where the DB credentials can be retrieved from<br />Credentials need to be stored in basic auth form | | |
#### DashboardList
@ -422,11 +403,11 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `supabaseAdmin` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | | | |
| `authenticator` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | | | |
| `supabaseAuthAdmin` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | | | |
| `supabaseFunctionsAdmin` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | | | |
| `supabaseStorageAdmin` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | | | |
| `supabaseAdmin` _string_ | | | |
| `authenticator` _string_ | | | |
| `supabaseAuthAdmin` _string_ | | | |
| `supabaseFunctionsAdmin` _string_ | | | |
| `supabaseStorageAdmin` _string_ | | | |
#### DatabaseStatus
@ -446,6 +427,25 @@ _Appears in:_
| `roles` _object (keys:string, values:integer array)_ | | | |
#### DbCredentialsReference
_Appears in:_
- [DashboardDbSpec](#dashboarddbspec)
- [StorageApiDbSpec](#storageapidbspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `secretName` _string_ | | | |
| `usernameKey` _string_ | UsernameKey | username | |
| `passwordKey` _string_ | PasswordKey | password | |
#### EmailAuthProvider
@ -484,7 +484,7 @@ _Appears in:_
| `host` _string_ | | | |
| `port` _integer_ | | | |
| `maxFrequency` _integer_ | | | |
| `credentialsFrom` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | | | |
| `credentialsRef` _[SmtpCredentialsReference](#smtpcredentialsreference)_ | | | |
#### EnvoySpec
@ -558,6 +558,28 @@ _Appears in:_
| `pullPolicy` _[PullPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#pullpolicy-v1-core)_ | | | |
#### JwtSpec
_Appears in:_
- [CoreJwtSpec](#corejwtspec)
- [StorageSpec](#storagespec)
- [StudioSpec](#studiospec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `secretName` _string_ | 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 | |
#### MigrationStatus
_Underlying type:_ _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_
@ -642,6 +664,60 @@ _Appears in:_
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the PostgREST workload | | |
#### S3CredentialsRef
_Appears in:_
- [S3ProtocolSpec](#s3protocolspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `secretName` _string_ | | | |
| `accessKeyIdKey` _string_ | AccessKeyIdKey - key in Secret where access key id will be referenced from | accessKeyId | |
| `accessSecretKeyKey` _string_ | AccessSecretKeyKey - key in Secret where access secret key will be referenced from | secretAccessKey | |
#### S3ProtocolSpec
_Appears in:_
- [StorageSpec](#storagespec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `region` _string_ | Region - S3 region to use in the API | us-east-1 | |
| `allowForwardedHeader` _boolean_ | AllowForwardedHeader | true | |
| `credentialsSecretRef` _[S3CredentialsRef](#s3credentialsref)_ | CredentialsSecretRef - reference to the Secret where access key id and access secret key are stored | | |
#### SmtpCredentialsReference
_Appears in:_
- [EmailAuthSmtpSpec](#emailauthsmtpspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `secretName` _string_ | | | |
| `usernameKey` _string_ | UsernameKey | username | |
| `passwordKey` _string_ | PasswordKey | password | |
#### Storage
@ -661,6 +737,42 @@ _Appears in:_
| `spec` _[StorageSpec](#storagespec)_ | | | |
#### StorageApiDbSpec
_Appears in:_
- [StorageSpec](#storagespec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `host` _string_ | | | |
| `port` _integer_ | Port - Database port, typically 5432 | 5432 | |
| `dbName` _string_ | | | |
| `dbCredentialsRef` _[DbCredentialsReference](#dbcredentialsreference)_ | DBCredentialsRef - reference to a Secret key where the DB credentials can be retrieved from<br />Credentials need to be stored in basic auth form | | |
#### StorageBackend
_Underlying type:_ _string_
_Appears in:_
- [StorageSpec](#storagespec)
| Field | Description |
| --- | --- |
| `file` | |
| `s3` | |
#### StorageList
@ -692,7 +804,12 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `foo` _string_ | Foo is an example field of Storage. Edit storage_types.go to remove/update | | |
| `backendType` _[StorageBackend](#storagebackend)_ | BackendType - backend storage type to use | | Enum: [s3 file] <br /> |
| `fileSizeLimit` _integer_ | FileSizeLimit - maximum file upload size in bytes | 52428800 | |
| `jwtAuth` _[JwtSpec](#jwtspec)_ | JwtAuth - Configure the JWT authentication parameters.<br />This includes where to retrieve anon and service key from as well as JWT secret and JWKS references<br />needed to validate JWTs send to the API | | |
| `db` _[StorageApiDbSpec](#storageapidbspec)_ | DBSpec - Configure access to the Postgres database<br />In most cases this will reference the supabase-storage-admin credentials secret provided by the Core resource | | |
| `s3` _[S3ProtocolSpec](#s3protocolspec)_ | S3 - Configure S3 protocol | | |
| `enableImageTransformation` _boolean_ | EnableImageTransformation - whether to deploy the image proxy<br />the image proxy scale images to lower resolutions on demand to reduce traffic for instance for mobile devices | | |
@ -710,7 +827,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `jwt` _[DashboardJwtSpec](#dashboardjwtspec)_ | | | |
| `jwt` _[JwtSpec](#jwtspec)_ | | | |
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the studio deployment | | |
| `gatewayServiceSelector` _object (keys:string, values:string)_ | GatewayServiceSelector - selector to find the service for the API gateway<br />Required to configure the API URL in the studio deployment<br />If you don't run multiple APIGateway instances in the same namespaces, the default will be fine | \{ app.kubernetes.io/component:api-gateway app.kubernetes.io/name:envoy \} | |
| `externalUrl` _string_ | APIExternalURL is referring to the URL where Supabase API will be available<br />Typically this is the ingress of the API gateway | | |

View file

@ -38,6 +38,7 @@ import (
"code.icb4dc0.de/prskr/supabase-operator/internal/db"
"code.icb4dc0.de/prskr/supabase-operator/internal/errx"
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
"code.icb4dc0.de/prskr/supabase-operator/internal/pw"
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
)
@ -161,11 +162,11 @@ func (r *CoreDbReconciler) ensureDbRolesSecrets(
)
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,
dbSpec.Roles.Secrets.Authenticator: supabase.DBRoleAuthenticator,
dbSpec.Roles.Secrets.AuthAdmin: supabase.DBRoleAuthAdmin,
dbSpec.Roles.Secrets.FunctionsAdmin: supabase.DBRoleFunctionsAdmin,
dbSpec.Roles.Secrets.StorageAdmin: supabase.DBRoleStorageAdmin,
dbSpec.Roles.Secrets.Admin: supabase.DBRoleSupabaseAdmin,
}
if core.Status.Database.Roles == nil {
@ -210,7 +211,7 @@ func (r *CoreDbReconciler) ensureDbRolesSecrets(
if role.String() == dsnUser {
credentialsSecret.Data[corev1.BasicAuthPasswordKey] = []byte(dsnPW)
} else {
credentialsSecret.Data[corev1.BasicAuthPasswordKey] = GeneratePW(24, nil)
credentialsSecret.Data[corev1.BasicAuthPasswordKey] = pw.GeneratePW(24, nil)
}
secretLogger.Info("Update database role to match secret credentials")

View file

@ -115,7 +115,7 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: core.Spec.Database.Roles.Secrets.AuthAdmin.Name,
Name: core.Spec.Database.Roles.Secrets.AuthAdmin,
},
Key: corev1.BasicAuthPasswordKey,
},

View file

@ -57,7 +57,7 @@ func (r *CoreJwtReconciler) Reconcile(ctx context.Context, req ctrl.Request) (re
}
jwtSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: core.Spec.JWT.SecretRef.Name, Namespace: core.Namespace},
ObjectMeta: metav1.ObjectMeta{Name: core.Spec.JWT.SecretName, Namespace: core.Namespace},
}
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, jwtSecret, func() error {

View file

@ -129,7 +129,7 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: core.Spec.Database.Roles.Secrets.Authenticator.Name,
Name: core.Spec.Database.Roles.Secrets.Authenticator,
},
Key: corev1.BasicAuthPasswordKey,
},

View file

@ -90,7 +90,7 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
dsnSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: dashboard.Spec.DBSpec.DBCredentialsRef.Name,
Name: dashboard.Spec.DBSpec.DBCredentialsRef.SecretName,
Namespace: dashboard.Namespace,
},
}

View file

@ -25,6 +25,9 @@ package controller
// +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
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=storages,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=storages/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=storages/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

View file

@ -33,16 +33,8 @@ type StorageReconciler struct {
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=storages,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=storages/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=storages/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 Storage 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.4/pkg/reconcile

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
package pw
import (
"bytes"

View file

@ -125,6 +125,30 @@ type studioDefaults struct {
APIPort int32
}
type storageEnvApiKeys struct {
AnonKey secretEnv
ServiceKey secretEnv
JwtSecret secretEnv
JwtJwks secretEnv
DatabaseDSN stringEnv
FileSizeLimit intEnv[uint64]
UploadFileSizeLimit intEnv[uint64]
UploadFileSizeLimitStandard intEnv[uint64]
StorageBackend stringEnv
TenantID fixedEnv
StorageS3Region stringEnv
GlobalS3Bucket fixedEnv
EnableImaageTransformation boolEnv
ImgProxyURL stringEnv
TusUrlPath fixedEnv
S3AccessKeyId secretEnv
S3AccessKeySecret secretEnv
S3ProtocolPrefix fixedEnv
S3AllowForwardedHeader boolEnv
}
type storageApiDefaults struct{}
type envoyDefaults struct {
ConfigKey string
UID, GID int64
@ -160,6 +184,7 @@ var ServiceConfig = struct {
Auth serviceConfig[authEnvKeys, authConfigDefaults]
PGMeta serviceConfig[pgMetaEnvKeys, pgMetaDefaults]
Studio serviceConfig[studioEnvKeys, studioDefaults]
Storage serviceConfig[storageEnvApiKeys, storageApiDefaults]
Envoy envoyServiceConfig
JWT jwtConfig
}{
@ -259,6 +284,31 @@ var ServiceConfig = struct {
APIPort: 3000,
},
},
Storage: serviceConfig[storageEnvApiKeys, storageApiDefaults]{
Name: "storage-api",
EnvKeys: storageEnvApiKeys{
AnonKey: "ANON_KEY",
ServiceKey: "SERVICE_KEY",
JwtSecret: "AUTH_JWT_SECRET",
JwtJwks: "AUTH_JWT_JWKS",
StorageBackend: "STORAGE_BACKEND",
DatabaseDSN: "DATABASE_URL",
FileSizeLimit: "FILE_SIZE_LIMIT",
UploadFileSizeLimit: "UPLOAD_FILE_SIZE_LIMIT",
UploadFileSizeLimitStandard: "UPLOAD_FILE_SIZE_LIMIT_STANDARD",
TenantID: fixedEnvOf("TENANT_ID", "stub"),
StorageS3Region: "STORAGE_S3_REGION",
GlobalS3Bucket: fixedEnvOf("GLOBAL_S3_BUCKET", "stub"),
EnableImaageTransformation: "ENABLE_IMAGE_TRANSFORMATION",
ImgProxyURL: "IMGPROXY_URL",
TusUrlPath: fixedEnvOf("TUS_URL_PATH", "/storage/v1/upload/resumable"),
S3AccessKeyId: "S3_PROTOCOL_ACCESS_KEY_ID",
S3AccessKeySecret: "S3_PROTOCOL_ACCESS_KEY_SECRET",
S3ProtocolPrefix: fixedEnvOf("S3_PROTOCOL_PREFIX", "/storage/v1"),
S3AllowForwardedHeader: "S3_ALLOW_FORWARDED_HEADER",
},
Defaults: storageApiDefaults{},
},
Envoy: envoyServiceConfig{
Defaults: envoyDefaults{
ConfigKey: "config.yaml",

View file

@ -63,44 +63,32 @@ func (d *CoreCustomDefaulter) Default(ctx context.Context, obj runtime.Object) e
return fmt.Errorf("ensuring JWT secret: %w", err)
}
// TODO copy plain text DSN to secret if present
corelog.Info("Defaulting database roles")
if !core.Spec.Database.Roles.SelfManaged {
const roleCredsSecretNameTemplate = "db-roles-creds-%s"
if core.Spec.Database.Roles.Secrets.Admin == nil {
const roleCredsSecretNameTemplate = "%s-db-creds-%s"
if core.Spec.Database.Roles.Secrets.Admin == "" {
corelog.Info("Defaulting role", "role_name", supabase.DBRoleSupabaseAdmin)
core.Spec.Database.Roles.Secrets.Admin = &corev1.LocalObjectReference{
Name: fmt.Sprintf(roleCredsSecretNameTemplate, supabase.DBRoleSupabaseAdmin.K8sString()),
}
core.Spec.Database.Roles.Secrets.Admin = fmt.Sprintf(roleCredsSecretNameTemplate, core.Name, supabase.DBRoleSupabaseAdmin.K8sString())
}
if core.Spec.Database.Roles.Secrets.Authenticator == nil {
if core.Spec.Database.Roles.Secrets.Authenticator == "" {
corelog.Info("Defaulting role", "role_name", supabase.DBRoleAuthenticator)
core.Spec.Database.Roles.Secrets.Authenticator = &corev1.LocalObjectReference{
Name: fmt.Sprintf(roleCredsSecretNameTemplate, supabase.DBRoleAuthenticator.K8sString()),
}
core.Spec.Database.Roles.Secrets.Authenticator = fmt.Sprintf(roleCredsSecretNameTemplate, core.Name, supabase.DBRoleAuthenticator.K8sString())
}
if core.Spec.Database.Roles.Secrets.AuthAdmin == nil {
if core.Spec.Database.Roles.Secrets.AuthAdmin == "" {
corelog.Info("Defaulting role", "role_name", supabase.DBRoleAuthAdmin)
core.Spec.Database.Roles.Secrets.AuthAdmin = &corev1.LocalObjectReference{
Name: fmt.Sprintf(roleCredsSecretNameTemplate, supabase.DBRoleAuthAdmin.K8sString()),
}
core.Spec.Database.Roles.Secrets.AuthAdmin = fmt.Sprintf(roleCredsSecretNameTemplate, core.Name, supabase.DBRoleAuthAdmin.K8sString())
}
if core.Spec.Database.Roles.Secrets.FunctionsAdmin == nil {
if core.Spec.Database.Roles.Secrets.FunctionsAdmin == "" {
corelog.Info("Defaulting role", "role_name", supabase.DBRoleFunctionsAdmin)
core.Spec.Database.Roles.Secrets.FunctionsAdmin = &corev1.LocalObjectReference{
Name: fmt.Sprintf(roleCredsSecretNameTemplate, supabase.DBRoleFunctionsAdmin.K8sString()),
}
core.Spec.Database.Roles.Secrets.FunctionsAdmin = fmt.Sprintf(roleCredsSecretNameTemplate, core.Name, supabase.DBRoleFunctionsAdmin.K8sString())
}
if core.Spec.Database.Roles.Secrets.StorageAdmin == nil {
if core.Spec.Database.Roles.Secrets.StorageAdmin == "" {
corelog.Info("Defaulting role", "role_name", supabase.DBRoleStorageAdmin)
core.Spec.Database.Roles.Secrets.StorageAdmin = &corev1.LocalObjectReference{
Name: fmt.Sprintf(roleCredsSecretNameTemplate, supabase.DBRoleStorageAdmin.K8sString()),
}
core.Spec.Database.Roles.Secrets.StorageAdmin = fmt.Sprintf(roleCredsSecretNameTemplate, core.Name, supabase.DBRoleStorageAdmin.K8sString())
}
}
@ -114,10 +102,8 @@ func (d *CoreCustomDefaulter) defaultJWT(ctx context.Context, core *supabasev1al
core.Spec.JWT = new(supabasev1alpha1.CoreJwtSpec)
}
if core.Spec.JWT.SecretRef == nil {
core.Spec.JWT.SecretRef = &corev1.LocalObjectReference{
Name: supabase.ServiceConfig.JWT.ObjectName(core),
}
if core.Spec.JWT.SecretName == "" {
core.Spec.JWT.SecretName = supabase.ServiceConfig.JWT.ObjectName(core)
}
if core.Spec.JWT.SecretKey == "" {
@ -138,7 +124,7 @@ func (d *CoreCustomDefaulter) defaultJWT(ctx context.Context, core *supabasev1al
jwtSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: core.Spec.JWT.SecretRef.Name,
Name: core.Spec.JWT.SecretName,
Namespace: core.Namespace,
},
}

View file

@ -127,47 +127,47 @@ func (v *CoreCustomValidator) validateDb(
}
}
if authenticator := dbSpec.Roles.Secrets.Authenticator; authenticator == nil {
if authenticator := dbSpec.Roles.Secrets.Authenticator; authenticator == "" {
return warnings, fmt.Errorf("%w: %s", ErrManagedCredentialsNotSpecified, supabase.DBRoleAuthenticator)
} else {
exists, err := doesSecretExists(ctx, authenticator.Name)
exists, err := doesSecretExists(ctx, authenticator)
if err != nil {
return warnings, err
} else if !exists {
return warnings, fmt.Errorf("%w: %s", ErrManagedCredentialsSecretMissing, authenticator.Name)
return warnings, fmt.Errorf("%w: %s", ErrManagedCredentialsSecretMissing, authenticator)
}
}
if authAdmin := dbSpec.Roles.Secrets.AuthAdmin; authAdmin == nil {
if authAdmin := dbSpec.Roles.Secrets.AuthAdmin; authAdmin == "" {
return warnings, fmt.Errorf("%w: %s", ErrManagedCredentialsNotSpecified, supabase.DBRoleAuthAdmin)
} else {
exists, err := doesSecretExists(ctx, authAdmin.Name)
exists, err := doesSecretExists(ctx, authAdmin)
if err != nil {
return warnings, err
} else if !exists {
return warnings, fmt.Errorf("%w: %s", ErrManagedCredentialsSecretMissing, authAdmin.Name)
return warnings, fmt.Errorf("%w: %s", ErrManagedCredentialsSecretMissing, authAdmin)
}
}
if functionsAdmin := dbSpec.Roles.Secrets.FunctionsAdmin; functionsAdmin == nil {
if functionsAdmin := dbSpec.Roles.Secrets.FunctionsAdmin; functionsAdmin == "" {
return warnings, fmt.Errorf("%w: %s", ErrManagedCredentialsNotSpecified, supabase.DBRoleFunctionsAdmin)
} else {
exists, err := doesSecretExists(ctx, functionsAdmin.Name)
exists, err := doesSecretExists(ctx, functionsAdmin)
if err != nil {
return warnings, err
} else if !exists {
return warnings, fmt.Errorf("%w: %s", ErrManagedCredentialsSecretMissing, functionsAdmin.Name)
return warnings, fmt.Errorf("%w: %s", ErrManagedCredentialsSecretMissing, functionsAdmin)
}
}
if storageAdmin := dbSpec.Roles.Secrets.StorageAdmin; storageAdmin == nil {
if storageAdmin := dbSpec.Roles.Secrets.StorageAdmin; storageAdmin == "" {
return warnings, fmt.Errorf("%w: %s", ErrManagedCredentialsNotSpecified, supabase.DBRoleStorageAdmin)
} else {
exists, err := doesSecretExists(ctx, storageAdmin.Name)
exists, err := doesSecretExists(ctx, storageAdmin)
if err != nil {
return warnings, err
} else if !exists {
return warnings, fmt.Errorf("%w: %s", ErrManagedCredentialsSecretMissing, storageAdmin.Name)
return warnings, fmt.Errorf("%w: %s", ErrManagedCredentialsSecretMissing, storageAdmin)
}
}
}

View file

@ -53,3 +53,11 @@ func SetupDashboardWebhookWithManager(mgr ctrl.Manager) error {
WithDefaulter(&DashboardCustomDefaulter{}).
Complete()
}
// SetupStorageWebhookWithManager registers the webhook for Storage in the manager.
func SetupStorageWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).For(&supabasev1alpha1.Storage{}).
WithValidator(&StorageCustomValidator{}).
WithDefaulter(&StorageCustomDefaulter{Client: mgr.GetClient()}).
Complete()
}

View file

@ -0,0 +1,109 @@
/*
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
"context"
"fmt"
"maps"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/webhook"
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
"code.icb4dc0.de/prskr/supabase-operator/internal/pw"
)
// +kubebuilder:webhook:path=/mutate-supabase-k8s-icb4dc0-de-v1alpha1-storage,mutating=true,failurePolicy=fail,sideEffects=None,groups=supabase.k8s.icb4dc0.de,resources=storages,verbs=create;update,versions=v1alpha1,name=mstorage-v1alpha1.kb.io,admissionReviewVersions=v1
// StorageCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind Storage when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type StorageCustomDefaulter struct {
client.Client
}
var _ webhook.CustomDefaulter = &StorageCustomDefaulter{}
// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Storage.
func (d *StorageCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
storage, ok := obj.(*supabasev1alpha1.Storage)
if !ok {
return fmt.Errorf("expected an Storage object but got %T", obj)
}
storagelog.Info("Defaulting for Storage", "name", storage.GetName())
if err := d.defaultS3Protocol(ctx, storage); err != nil {
return err
}
return nil
}
func (d *StorageCustomDefaulter) defaultS3Protocol(ctx context.Context, storage *supabasev1alpha1.Storage) error {
if storage.Spec.S3 == nil {
storage.Spec.S3 = new(supabasev1alpha1.S3ProtocolSpec)
}
if storage.Spec.S3.CredentialsSecretRef == nil {
storage.Spec.S3.CredentialsSecretRef = &supabasev1alpha1.S3CredentialsRef{
AccessKeyIdKey: "accessKeyId",
AccessSecretKeyKey: "secretAccessKey",
SecretName: fmt.Sprintf("%s-storage-protocol-s3-credentials", storage.Name),
}
}
credentialsSecret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: storage.Spec.S3.CredentialsSecretRef.SecretName,
Namespace: storage.Namespace,
},
}
_, err := controllerutil.CreateOrUpdate(ctx, d.Client, &credentialsSecret, func() error {
credentialsSecret.Labels = maps.Clone(storage.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, 2)
}
if _, ok := credentialsSecret.Data[storage.Spec.S3.CredentialsSecretRef.AccessKeyIdKey]; !ok {
credentialsSecret.Data[storage.Spec.S3.CredentialsSecretRef.AccessKeyIdKey] = pw.GeneratePW(32, nil)
}
if _, ok := credentialsSecret.Data[storage.Spec.S3.CredentialsSecretRef.AccessSecretKeyKey]; !ok {
credentialsSecret.Data[storage.Spec.S3.CredentialsSecretRef.AccessSecretKeyKey] = pw.GeneratePW(64, nil)
}
return nil
})
return err
}

View file

@ -0,0 +1,86 @@
/*
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
// TODO (user): Add any additional imports if needed
)
var _ = Describe("Storage Webhook", func() {
var (
obj *supabasev1alpha1.Storage
oldObj *supabasev1alpha1.Storage
validator StorageCustomValidator
defaulter StorageCustomDefaulter
)
BeforeEach(func() {
obj = &supabasev1alpha1.Storage{}
oldObj = &supabasev1alpha1.Storage{}
validator = StorageCustomValidator{}
Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
defaulter = StorageCustomDefaulter{}
Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
// TODO (user): Add any setup logic common to all tests
})
AfterEach(func() {
// TODO (user): Add any teardown logic common to all tests
})
Context("When creating Storage under Defaulting Webhook", func() {
// TODO (user): Add logic for defaulting webhooks
// Example:
// It("Should apply defaults when a required field is empty", func() {
// By("simulating a scenario where defaults should be applied")
// obj.SomeFieldWithDefault = ""
// By("calling the Default method to apply defaults")
// defaulter.Default(ctx, obj)
// By("checking that the default values are set")
// Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
// })
})
Context("When creating or updating Storage under Validating Webhook", func() {
// TODO (user): Add logic for validating webhooks
// Example:
// It("Should deny creation if a required field is missing", func() {
// By("simulating an invalid creation scenario")
// obj.SomeRequiredField = ""
// Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
// })
//
// It("Should admit creation if all required fields are present", func() {
// By("simulating an invalid creation scenario")
// obj.SomeRequiredField = "valid_value"
// Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
// })
//
// It("Should validate updates correctly", func() {
// By("simulating a valid update scenario")
// oldObj.SomeRequiredField = "updated_value"
// obj.SomeRequiredField = "updated_value"
// Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
// })
})
})

View file

@ -0,0 +1,87 @@
/*
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
)
// nolint:unused
// log is for logging in this package.
var storagelog = logf.Log.WithName("storage-resource")
// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here.
// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook.
// +kubebuilder:webhook:path=/validate-supabase-k8s-icb4dc0-de-v1alpha1-storage,mutating=false,failurePolicy=fail,sideEffects=None,groups=supabase.k8s.icb4dc0.de,resources=storages,verbs=create;update,versions=v1alpha1,name=vstorage-v1alpha1.kb.io,admissionReviewVersions=v1
// StorageCustomValidator struct is responsible for validating the Storage resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type StorageCustomValidator struct {
// TODO(user): Add more fields as needed for validation
}
var _ webhook.CustomValidator = &StorageCustomValidator{}
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Storage.
func (v *StorageCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
storage, ok := obj.(*supabasev1alpha1.Storage)
if !ok {
return nil, fmt.Errorf("expected a Storage object but got %T", obj)
}
storagelog.Info("Validation for Storage upon creation", "name", storage.GetName())
// TODO(user): fill in your validation logic upon object creation.
return nil, nil
}
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Storage.
func (v *StorageCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
storage, ok := newObj.(*supabasev1alpha1.Storage)
if !ok {
return nil, fmt.Errorf("expected a Storage object for the newObj but got %T", newObj)
}
storagelog.Info("Validation for Storage upon update", "name", storage.GetName())
// TODO(user): fill in your validation logic upon object update.
return nil, nil
}
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Storage.
func (v *StorageCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
storage, ok := obj.(*supabasev1alpha1.Storage)
if !ok {
return nil, fmt.Errorf("expected a Storage object but got %T", obj)
}
storagelog.Info("Validation for Storage upon deletion", "name", storage.GetName())
// TODO(user): fill in your validation logic upon object deletion.
return nil, nil
}

View file

@ -127,6 +127,9 @@ var _ = BeforeSuite(func() {
err = SetupDashboardWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())
err = SetupStorageWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:webhook
go func() {