From b55afea477651b240a84af23157bc6f544df7984 Mon Sep 17 00:00:00 2001 From: Peter Kurfer Date: Tue, 21 Jan 2025 21:54:53 +0100 Subject: [PATCH] feat(storage): prepare custom resource for storage API --- PROJECT | 4 + Tiltfile | 8 + api/v1alpha1/common_types.go | 44 +++++ api/v1alpha1/core_types.go | 75 ++++---- api/v1alpha1/dashboard_types.go | 61 ++---- api/v1alpha1/storage_types.go | 89 +++++++-- api/v1alpha1/zz_generated.deepcopy.go | 178 ++++++++++++------ cmd/manager.go | 7 +- config/certmanager/certificate-metrics.yaml | 20 ++ config/certmanager/certificate-webhook.yaml | 20 ++ config/certmanager/issuer.yaml | 13 ++ .../bases/supabase.k8s.icb4dc0.de_cores.yaml | 124 +++--------- .../supabase.k8s.icb4dc0.de_dashboards.yaml | 40 ++-- .../supabase.k8s.icb4dc0.de_storages.yaml | 119 +++++++++++- .../samples/supabase_v1alpha1_dashboard.yaml | 5 +- config/samples/supabase_v1alpha1_storage.yaml | 12 +- config/webhook/manifests.yaml | 40 ++++ docs/api/supabase.k8s.icb4dc0.de.md | 177 ++++++++++++++--- internal/controller/core_db_controller.go | 13 +- internal/controller/core_gotrue_controller.go | 2 +- internal/controller/core_jwt_controller.go | 2 +- .../controller/core_postgrest_controller.go | 2 +- .../dashboard_pg-meta_controller.go | 2 +- internal/controller/permissions.go | 3 + internal/controller/storage_controller.go | 8 - internal/{controller => pw}/pwgen.go | 2 +- internal/supabase/env.go | 50 +++++ .../v1alpha1/core_webhook_defaulter.go | 42 ++--- .../v1alpha1/core_webhook_validator.go | 24 +-- internal/webhook/v1alpha1/setup.go | 8 + .../v1alpha1/storage_webhook_defaulter.go | 109 +++++++++++ .../webhook/v1alpha1/storage_webhook_test.go | 86 +++++++++ .../v1alpha1/storage_webhook_validator.go | 87 +++++++++ .../webhook/v1alpha1/webhook_suite_test.go | 3 + 34 files changed, 1110 insertions(+), 369 deletions(-) create mode 100644 config/certmanager/certificate-metrics.yaml create mode 100644 config/certmanager/certificate-webhook.yaml create mode 100644 config/certmanager/issuer.yaml rename internal/{controller => pw}/pwgen.go (98%) create mode 100644 internal/webhook/v1alpha1/storage_webhook_defaulter.go create mode 100644 internal/webhook/v1alpha1/storage_webhook_test.go create mode 100644 internal/webhook/v1alpha1/storage_webhook_validator.go diff --git a/PROJECT b/PROJECT index 49a57b9..89fa4cd 100644 --- a/PROJECT +++ b/PROJECT @@ -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" diff --git a/Tiltfile b/Tiltfile index 48abc96..53db91e 100644 --- a/Tiltfile +++ b/Tiltfile @@ -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' + ], +) diff --git a/api/v1alpha1/common_types.go b/api/v1alpha1/common_types.go index 512415e..0ffdf97 100644 --- a/api/v1alpha1/common_types.go +++ b/api/v1alpha1/common_types.go @@ -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"` diff --git a/api/v1alpha1/core_types.go b/api/v1alpha1/core_types.go index 4f2fe2c..15b2dca 100644 --- a/api/v1alpha1/core_types.go +++ b/api/v1alpha1/core_types.go @@ -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, }, }, }, diff --git a/api/v1alpha1/dashboard_types.go b/api/v1alpha1/dashboard_types.go index 91e805a..2b4cd29 100644 --- a/api/v1alpha1/dashboard_types.go +++ b/api/v1alpha1/dashboard_types.go @@ -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, } } diff --git a/api/v1alpha1/storage_types.go b/api/v1alpha1/storage_types.go index 3b6d376..fede7be 100644 --- a/api/v1alpha1/storage_types.go +++ b/api/v1alpha1/storage_types.go @@ -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 diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 5b91986..00dbad2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -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 diff --git a/cmd/manager.go b/cmd/manager.go index 37edf3c..8d64bc2 100644 --- a/cmd/manager.go +++ b/cmd/manager.go @@ -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 diff --git a/config/certmanager/certificate-metrics.yaml b/config/certmanager/certificate-metrics.yaml new file mode 100644 index 0000000..259c57a --- /dev/null +++ b/config/certmanager/certificate-metrics.yaml @@ -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 diff --git a/config/certmanager/certificate-webhook.yaml b/config/certmanager/certificate-webhook.yaml new file mode 100644 index 0000000..05e595a --- /dev/null +++ b/config/certmanager/certificate-webhook.yaml @@ -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 diff --git a/config/certmanager/issuer.yaml b/config/certmanager/issuer.yaml new file mode 100644 index 0000000..1dd9806 --- /dev/null +++ b/config/certmanager/issuer.yaml @@ -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: {} diff --git a/config/crd/bases/supabase.k8s.icb4dc0.de_cores.yaml b/config/crd/bases/supabase.k8s.icb4dc0.de_cores.yaml index 249b8f6..4a407a6 100644 --- a/config/crd/bases/supabase.k8s.icb4dc0.de_cores.yaml +++ b/config/crd/bases/supabase.k8s.icb4dc0.de_cores.yaml @@ -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 diff --git a/config/crd/bases/supabase.k8s.icb4dc0.de_dashboards.yaml b/config/crd/bases/supabase.k8s.icb4dc0.de_dashboards.yaml index 18b824e..56cd9f5 100644 --- a/config/crd/bases/supabase.k8s.icb4dc0.de_dashboards.yaml +++ b/config/crd/bases/supabase.k8s.icb4dc0.de_dashboards.yaml @@ -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 diff --git a/config/crd/bases/supabase.k8s.icb4dc0.de_storages.yaml b/config/crd/bases/supabase.k8s.icb4dc0.de_storages.yaml index 95beb91..a164cbd 100644 --- a/config/crd/bases/supabase.k8s.icb4dc0.de_storages.yaml +++ b/config/crd/bases/supabase.k8s.icb4dc0.de_storages.yaml @@ -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. diff --git a/config/samples/supabase_v1alpha1_dashboard.yaml b/config/samples/supabase_v1alpha1_dashboard.yaml index c9b451e..6da3406 100644 --- a/config/samples/supabase_v1alpha1_dashboard.yaml +++ b/config/samples/supabase_v1alpha1_dashboard.yaml @@ -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 diff --git a/config/samples/supabase_v1alpha1_storage.yaml b/config/samples/supabase_v1alpha1_storage.yaml index fc5cd56..75591f0 100644 --- a/config/samples/supabase_v1alpha1_storage.yaml +++ b/config/samples/supabase_v1alpha1_storage.yaml @@ -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 diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 8e3eda8..08bd8e7 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -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 diff --git a/docs/api/supabase.k8s.icb4dc0.de.md b/docs/api/supabase.k8s.icb4dc0.de.md index 6411f2a..1687f60 100644 --- a/docs/api/supabase.k8s.icb4dc0.de.md +++ b/docs/api/supabase.k8s.icb4dc0.de.md @@ -229,12 +229,12 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `secret` _string_ | Secret - JWT HMAC secret in plain text
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
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
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
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
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]
| +| `fileSizeLimit` _integer_ | FileSizeLimit - maximum file upload size in bytes | 52428800 | | +| `jwtAuth` _[JwtSpec](#jwtspec)_ | 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 | | | +| `db` _[StorageApiDbSpec](#storageapidbspec)_ | DBSpec - Configure access to the Postgres database
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
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
Required to configure the API URL in the studio deployment
If you don't run multiple APIGateway instances in the same namespaces, the default will be fine | \{ 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
Typically this is the ingress of the API gateway | | | diff --git a/internal/controller/core_db_controller.go b/internal/controller/core_db_controller.go index 440d362..3ab2e9e 100644 --- a/internal/controller/core_db_controller.go +++ b/internal/controller/core_db_controller.go @@ -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") diff --git a/internal/controller/core_gotrue_controller.go b/internal/controller/core_gotrue_controller.go index 33052d0..7e1a30f 100644 --- a/internal/controller/core_gotrue_controller.go +++ b/internal/controller/core_gotrue_controller.go @@ -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, }, diff --git a/internal/controller/core_jwt_controller.go b/internal/controller/core_jwt_controller.go index ffd93e5..122658f 100644 --- a/internal/controller/core_jwt_controller.go +++ b/internal/controller/core_jwt_controller.go @@ -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 { diff --git a/internal/controller/core_postgrest_controller.go b/internal/controller/core_postgrest_controller.go index 4acb962..b3618af 100644 --- a/internal/controller/core_postgrest_controller.go +++ b/internal/controller/core_postgrest_controller.go @@ -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, }, diff --git a/internal/controller/dashboard_pg-meta_controller.go b/internal/controller/dashboard_pg-meta_controller.go index 1e9e9e5..9378bb1 100644 --- a/internal/controller/dashboard_pg-meta_controller.go +++ b/internal/controller/dashboard_pg-meta_controller.go @@ -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, }, } diff --git a/internal/controller/permissions.go b/internal/controller/permissions.go index 5a704b0..6be4cbe 100644 --- a/internal/controller/permissions.go +++ b/internal/controller/permissions.go @@ -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 diff --git a/internal/controller/storage_controller.go b/internal/controller/storage_controller.go index 084af06..5be2de5 100644 --- a/internal/controller/storage_controller.go +++ b/internal/controller/storage_controller.go @@ -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 diff --git a/internal/controller/pwgen.go b/internal/pw/pwgen.go similarity index 98% rename from internal/controller/pwgen.go rename to internal/pw/pwgen.go index 5c7d762..867d47a 100644 --- a/internal/controller/pwgen.go +++ b/internal/pw/pwgen.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package pw import ( "bytes" diff --git a/internal/supabase/env.go b/internal/supabase/env.go index 3aeefa6..d92032f 100644 --- a/internal/supabase/env.go +++ b/internal/supabase/env.go @@ -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", diff --git a/internal/webhook/v1alpha1/core_webhook_defaulter.go b/internal/webhook/v1alpha1/core_webhook_defaulter.go index f588bcd..3604bd4 100644 --- a/internal/webhook/v1alpha1/core_webhook_defaulter.go +++ b/internal/webhook/v1alpha1/core_webhook_defaulter.go @@ -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, }, } diff --git a/internal/webhook/v1alpha1/core_webhook_validator.go b/internal/webhook/v1alpha1/core_webhook_validator.go index ebaed8a..96c76c4 100644 --- a/internal/webhook/v1alpha1/core_webhook_validator.go +++ b/internal/webhook/v1alpha1/core_webhook_validator.go @@ -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) } } } diff --git a/internal/webhook/v1alpha1/setup.go b/internal/webhook/v1alpha1/setup.go index 685ec21..d14bb8f 100644 --- a/internal/webhook/v1alpha1/setup.go +++ b/internal/webhook/v1alpha1/setup.go @@ -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() +} diff --git a/internal/webhook/v1alpha1/storage_webhook_defaulter.go b/internal/webhook/v1alpha1/storage_webhook_defaulter.go new file mode 100644 index 0000000..d86da94 --- /dev/null +++ b/internal/webhook/v1alpha1/storage_webhook_defaulter.go @@ -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 +} diff --git a/internal/webhook/v1alpha1/storage_webhook_test.go b/internal/webhook/v1alpha1/storage_webhook_test.go new file mode 100644 index 0000000..a7860b2 --- /dev/null +++ b/internal/webhook/v1alpha1/storage_webhook_test.go @@ -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()) + // }) + }) +}) diff --git a/internal/webhook/v1alpha1/storage_webhook_validator.go b/internal/webhook/v1alpha1/storage_webhook_validator.go new file mode 100644 index 0000000..e252bc2 --- /dev/null +++ b/internal/webhook/v1alpha1/storage_webhook_validator.go @@ -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 +} diff --git a/internal/webhook/v1alpha1/webhook_suite_test.go b/internal/webhook/v1alpha1/webhook_suite_test.go index c0d6967..1f0d1fe 100644 --- a/internal/webhook/v1alpha1/webhook_suite_test.go +++ b/internal/webhook/v1alpha1/webhook_suite_test.go @@ -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() {