diff --git a/internal/controller/supabaseproject_controller.go b/internal/controller/supabaseproject_controller.go index 577cb47..17292d0 100644 --- a/internal/controller/supabaseproject_controller.go +++ b/internal/controller/supabaseproject_controller.go @@ -47,6 +47,34 @@ const ( RequeueDelay = 10 * time.Second ) +// serviceReconcileConfig holds configuration for reconciling a service component. +type serviceReconcileConfig struct { + name string + conditionType string + buildDeployment func() *appsv1.Deployment + buildService func() *corev1.Service + setStatus func(ready bool) + logFields []any // optional +} + +// newServiceReconcileConfig creates a serviceReconcileConfig with required fields as parameters. +// This ensures callers cannot forget to provide required values (compile-time enforcement). +func newServiceReconcileConfig( + name string, + conditionType string, + buildDeployment func() *appsv1.Deployment, + buildService func() *corev1.Service, + setStatus func(ready bool), +) serviceReconcileConfig { + return serviceReconcileConfig{ + name: name, + conditionType: conditionType, + buildDeployment: buildDeployment, + buildService: buildService, + setStatus: setStatus, + } +} + // SupabaseProjectReconciler reconciles a SupabaseProject object type SupabaseProjectReconciler struct { client.Client @@ -419,20 +447,16 @@ func (r *SupabaseProjectReconciler) reconcileServices(ctx context.Context, proje return nil } -// reconcileAuth deploys the Auth service -func (r *SupabaseProjectReconciler) reconcileAuth(ctx context.Context, project *supabasev1alpha1.SupabaseProject, secretNames *supabasev1alpha1.SecretNamesStatus) error { +// reconcileServiceComponent is a generic helper for reconciling a service component (deployment + service) +func (r *SupabaseProjectReconciler) reconcileServiceComponent(ctx context.Context, project *supabasev1alpha1.SupabaseProject, config serviceReconcileConfig) error { log := logf.FromContext(ctx) - log.Info("Reconciling Auth service") + log.Info(fmt.Sprintf("Reconciling %s service", config.name)) // Create deployment - deployment := deployments.BuildAuthDeployment(project, secretNames) - log.V(1).Info("Built Auth deployment", - "image", fmt.Sprintf("%s:%s", "supabase/gotrue", project.Spec.Auth.ImageTag), - "replicas", project.Spec.Auth.Replicas, - "hasProviders", project.Spec.Auth.Providers != nil, - "hasEmailHook", project.Spec.Auth.EmailHook != nil && project.Spec.Auth.EmailHook.Enabled) + deployment := config.buildDeployment() + log.V(1).Info(fmt.Sprintf("Built %s deployment", config.name), config.logFields...) if err := r.createOrUpdateDeployment(ctx, project, deployment); err != nil { - r.setCondition(project, supabasev1alpha1.ConditionTypeAuthReady, metav1.ConditionFalse, "DeploymentFailed", err.Error()) + r.setCondition(project, config.conditionType, metav1.ConditionFalse, "DeploymentFailed", err.Error()) if statusErr := r.Status().Update(ctx, project); statusErr != nil { return statusErr } @@ -440,116 +464,83 @@ func (r *SupabaseProjectReconciler) reconcileAuth(ctx context.Context, project * } // Create service - service := services.BuildAuthService(project) + service := config.buildService() if err := r.createOrUpdateService(ctx, project, service); err != nil { - r.setCondition(project, supabasev1alpha1.ConditionTypeAuthReady, metav1.ConditionFalse, "ServiceFailed", err.Error()) + r.setCondition(project, config.conditionType, metav1.ConditionFalse, "ServiceFailed", err.Error()) if statusErr := r.Status().Update(ctx, project); statusErr != nil { return statusErr } return err } - project.Status.Services.Auth = supabasev1alpha1.ServiceStatus{Ready: true} - r.setCondition(project, supabasev1alpha1.ConditionTypeAuthReady, metav1.ConditionTrue, "Ready", "Auth service is running") + config.setStatus(true) + r.setCondition(project, config.conditionType, metav1.ConditionTrue, "Ready", fmt.Sprintf("%s service is running", config.name)) return nil } +// reconcileAuth deploys the Auth service +func (r *SupabaseProjectReconciler) reconcileAuth(ctx context.Context, project *supabasev1alpha1.SupabaseProject, secretNames *supabasev1alpha1.SecretNamesStatus) error { + config := newServiceReconcileConfig( + "Auth", + supabasev1alpha1.ConditionTypeAuthReady, + func() *appsv1.Deployment { return deployments.BuildAuthDeployment(project, secretNames) }, + func() *corev1.Service { return services.BuildAuthService(project) }, + func(ready bool) { project.Status.Services.Auth = supabasev1alpha1.ServiceStatus{Ready: ready} }, + ) + config.logFields = []any{ + "image", fmt.Sprintf("%s:%s", "supabase/gotrue", project.Spec.Auth.ImageTag), + "replicas", project.Spec.Auth.Replicas, + "hasProviders", project.Spec.Auth.Providers != nil, + "hasEmailHook", project.Spec.Auth.EmailHook != nil && project.Spec.Auth.EmailHook.Enabled, + } + return r.reconcileServiceComponent(ctx, project, config) +} + // reconcileRest deploys the REST service func (r *SupabaseProjectReconciler) reconcileRest(ctx context.Context, project *supabasev1alpha1.SupabaseProject, secretNames *supabasev1alpha1.SecretNamesStatus) error { - log := logf.FromContext(ctx) - log.Info("Reconciling REST service") - - // Create deployment - deployment := deployments.BuildRestDeployment(project, secretNames) - log.V(1).Info("Built REST deployment", + config := newServiceReconcileConfig( + "REST", + supabasev1alpha1.ConditionTypeRestReady, + func() *appsv1.Deployment { return deployments.BuildRestDeployment(project, secretNames) }, + func() *corev1.Service { return services.BuildRestService(project) }, + func(ready bool) { project.Status.Services.Rest = supabasev1alpha1.ServiceStatus{Ready: ready} }, + ) + config.logFields = []any{ "image", fmt.Sprintf("%s:%s", "postgrest/postgrest", project.Spec.Rest.ImageTag), - "schemas", project.Spec.Rest.Schemas) - if err := r.createOrUpdateDeployment(ctx, project, deployment); err != nil { - r.setCondition(project, supabasev1alpha1.ConditionTypeRestReady, metav1.ConditionFalse, "DeploymentFailed", err.Error()) - if statusErr := r.Status().Update(ctx, project); statusErr != nil { - return statusErr - } - return err - } - - // Create service - service := services.BuildRestService(project) - if err := r.createOrUpdateService(ctx, project, service); err != nil { - r.setCondition(project, supabasev1alpha1.ConditionTypeRestReady, metav1.ConditionFalse, "ServiceFailed", err.Error()) - if statusErr := r.Status().Update(ctx, project); statusErr != nil { - return statusErr - } - return err + "schemas", project.Spec.Rest.Schemas, } - - project.Status.Services.Rest = supabasev1alpha1.ServiceStatus{Ready: true} - r.setCondition(project, supabasev1alpha1.ConditionTypeRestReady, metav1.ConditionTrue, "Ready", "REST service is running") - return nil + return r.reconcileServiceComponent(ctx, project, config) } // reconcileStudio deploys the Studio service func (r *SupabaseProjectReconciler) reconcileStudio(ctx context.Context, project *supabasev1alpha1.SupabaseProject, secretNames *supabasev1alpha1.SecretNamesStatus) error { - log := logf.FromContext(ctx) - log.Info("Reconciling Studio service") - - // Create deployment - deployment := deployments.BuildStudioDeployment(project, secretNames) - log.V(1).Info("Built Studio deployment", + config := newServiceReconcileConfig( + "Studio", + supabasev1alpha1.ConditionTypeStudioReady, + func() *appsv1.Deployment { return deployments.BuildStudioDeployment(project, secretNames) }, + func() *corev1.Service { return services.BuildStudioService(project) }, + func(ready bool) { project.Status.Services.Studio = supabasev1alpha1.ServiceStatus{Ready: ready} }, + ) + config.logFields = []any{ "image", fmt.Sprintf("%s:%s", "supabase/studio", project.Spec.Studio.ImageTag), - "publicURL", project.Spec.Studio.PublicURL) - if err := r.createOrUpdateDeployment(ctx, project, deployment); err != nil { - r.setCondition(project, supabasev1alpha1.ConditionTypeStudioReady, metav1.ConditionFalse, "DeploymentFailed", err.Error()) - if statusErr := r.Status().Update(ctx, project); statusErr != nil { - return statusErr - } - return err - } - - // Create service - service := services.BuildStudioService(project) - if err := r.createOrUpdateService(ctx, project, service); err != nil { - r.setCondition(project, supabasev1alpha1.ConditionTypeStudioReady, metav1.ConditionFalse, "ServiceFailed", err.Error()) - if statusErr := r.Status().Update(ctx, project); statusErr != nil { - return statusErr - } - return err + "publicURL", project.Spec.Studio.PublicURL, } - - project.Status.Services.Studio = supabasev1alpha1.ServiceStatus{Ready: true} - r.setCondition(project, supabasev1alpha1.ConditionTypeStudioReady, metav1.ConditionTrue, "Ready", "Studio service is running") - return nil + return r.reconcileServiceComponent(ctx, project, config) } // reconcileMeta deploys the Meta service func (r *SupabaseProjectReconciler) reconcileMeta(ctx context.Context, project *supabasev1alpha1.SupabaseProject, secretNames *supabasev1alpha1.SecretNamesStatus) error { - log := logf.FromContext(ctx) - log.Info("Reconciling Meta service") - - // Create deployment - deployment := deployments.BuildMetaDeployment(project, secretNames) - log.V(1).Info("Built Meta deployment", - "image", fmt.Sprintf("%s:%s", "supabase/postgres-meta", project.Spec.Meta.ImageTag)) - if err := r.createOrUpdateDeployment(ctx, project, deployment); err != nil { - r.setCondition(project, supabasev1alpha1.ConditionTypeMetaReady, metav1.ConditionFalse, "DeploymentFailed", err.Error()) - if statusErr := r.Status().Update(ctx, project); statusErr != nil { - return statusErr - } - return err - } - - // Create service - service := services.BuildMetaService(project) - if err := r.createOrUpdateService(ctx, project, service); err != nil { - r.setCondition(project, supabasev1alpha1.ConditionTypeMetaReady, metav1.ConditionFalse, "ServiceFailed", err.Error()) - if statusErr := r.Status().Update(ctx, project); statusErr != nil { - return statusErr - } - return err - } - - project.Status.Services.Meta = supabasev1alpha1.ServiceStatus{Ready: true} - r.setCondition(project, supabasev1alpha1.ConditionTypeMetaReady, metav1.ConditionTrue, "Ready", "Meta service is running") - return nil + config := newServiceReconcileConfig( + "Meta", + supabasev1alpha1.ConditionTypeMetaReady, + func() *appsv1.Deployment { return deployments.BuildMetaDeployment(project, secretNames) }, + func() *corev1.Service { return services.BuildMetaService(project) }, + func(ready bool) { project.Status.Services.Meta = supabasev1alpha1.ServiceStatus{Ready: ready} }, + ) + config.logFields = []any{ + "image", fmt.Sprintf("%s:%s", "supabase/postgres-meta", project.Spec.Meta.ImageTag), + } + return r.reconcileServiceComponent(ctx, project, config) } // reconcileKong deploys the Kong API gateway diff --git a/internal/resources/common/labels.go b/internal/resources/common/labels.go index f05aae7..e0eb129 100644 --- a/internal/resources/common/labels.go +++ b/internal/resources/common/labels.go @@ -41,8 +41,16 @@ const ( // DatabaseName is the default database name for Supabase DatabaseName = "supabase" + + // ReloaderAnnotation is the annotation key for stakater/reloader auto-reload + ReloaderAnnotation = "reloader.stakater.com/auto" ) +// ReloaderAnnotations returns the annotations map for enabling stakater/reloader +func ReloaderAnnotations() map[string]string { + return map[string]string{ReloaderAnnotation: "true"} +} + // CommonLabels returns the common labels for all resources func CommonLabels(project *supabasev1alpha1.SupabaseProject) map[string]string { return map[string]string{ diff --git a/internal/resources/common/names.go b/internal/resources/common/names.go new file mode 100644 index 0000000..acb6a0c --- /dev/null +++ b/internal/resources/common/names.go @@ -0,0 +1,26 @@ +/* +Copyright 2026 GuionAI. + +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 common + +import ( + supabasev1alpha1 "github.com/GuionAI/cloudnative-supabase/api/v1alpha1" +) + +// KongConfigMapName returns the kong config map name +func KongConfigMapName(project *supabasev1alpha1.SupabaseProject) string { + return project.Name + "-kong-config" +} diff --git a/internal/resources/configmaps/kong.go b/internal/resources/configmaps/kong.go index e58c85c..8044e1e 100644 --- a/internal/resources/configmaps/kong.go +++ b/internal/resources/configmaps/kong.go @@ -26,11 +26,6 @@ import ( "github.com/GuionAI/cloudnative-supabase/internal/resources/common" ) -// KongConfigMapName returns the kong config map name -func KongConfigMapName(project *supabasev1alpha1.SupabaseProject) string { - return project.Name + "-kong-config" -} - // BuildKongConfigMap creates the Kong declarative configuration ConfigMap func BuildKongConfigMap(project *supabasev1alpha1.SupabaseProject) *corev1.ConfigMap { // Build service names @@ -193,7 +188,7 @@ services: return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: KongConfigMapName(project), + Name: common.KongConfigMapName(project), Namespace: project.Namespace, Labels: common.ComponentLabels(project, "kong"), }, diff --git a/internal/resources/deployments/auth.go b/internal/resources/deployments/auth.go index 140c750..f57ba9e 100644 --- a/internal/resources/deployments/auth.go +++ b/internal/resources/deployments/auth.go @@ -22,7 +22,6 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" supabasev1alpha1 "github.com/GuionAI/cloudnative-supabase/api/v1alpha1" "github.com/GuionAI/cloudnative-supabase/internal/resources/cnpg" @@ -75,19 +74,14 @@ func BuildAuthDeployment(project *supabasev1alpha1.SupabaseProject, secretNames }) } - replicas := spec.Replicas - if replicas == 0 { - replicas = 1 - } + replicas := NormalizeReplicas(spec.Replicas) deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: project.Namespace, - Labels: common.ComponentLabels(project, AuthComponentName), - Annotations: map[string]string{ - "reloader.stakater.com/auto": "true", - }, + Name: name, + Namespace: project.Namespace, + Labels: common.ComponentLabels(project, AuthComponentName), + Annotations: common.ReloaderAnnotations(), }, Spec: appsv1.DeploymentSpec{ Replicas: &replicas, @@ -96,10 +90,8 @@ func BuildAuthDeployment(project *supabasev1alpha1.SupabaseProject, secretNames }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: common.ComponentLabels(project, AuthComponentName), - Annotations: map[string]string{ - "reloader.stakater.com/auto": "true", - }, + Labels: common.ComponentLabels(project, AuthComponentName), + Annotations: common.ReloaderAnnotations(), }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ @@ -116,31 +108,9 @@ func BuildAuthDeployment(project *supabasev1alpha1.SupabaseProject, secretNames Protocol: corev1.ProtocolTCP, }, }, - LivenessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/health", - Port: intstr.FromInt(AuthPort), - }, - }, - InitialDelaySeconds: 10, - PeriodSeconds: 10, - TimeoutSeconds: 5, - FailureThreshold: 3, - }, - ReadinessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/health", - Port: intstr.FromInt(AuthPort), - }, - }, - InitialDelaySeconds: 5, - PeriodSeconds: 5, - TimeoutSeconds: 3, - FailureThreshold: 3, - }, - Resources: spec.Resources, + LivenessProbe: BuildLivenessProbe("/health", AuthPort), + ReadinessProbe: BuildReadinessProbe("/health", AuthPort), + Resources: spec.Resources, }, }, }, @@ -148,10 +118,7 @@ func BuildAuthDeployment(project *supabasev1alpha1.SupabaseProject, secretNames }, } - // Add image pull secrets if specified - if len(project.Spec.ImagePullSecrets) > 0 { - deployment.Spec.Template.Spec.ImagePullSecrets = project.Spec.ImagePullSecrets - } + AddImagePullSecrets(&deployment.Spec.Template.Spec, project) return deployment } @@ -160,37 +127,31 @@ func BuildAuthDeployment(project *supabasev1alpha1.SupabaseProject, secretNames func buildAuthEnv(project *supabasev1alpha1.SupabaseProject, secretNames *supabasev1alpha1.SecretNamesStatus, dbHost string) []corev1.EnvVar { spec := project.Spec.Auth - env := []corev1.EnvVar{ - // Database configuration - {Name: "DB_HOST", Value: dbHost}, - {Name: "DB_PORT", Value: "5432"}, - {Name: "DB_DRIVER", Value: "postgres"}, - {Name: "DB_SSL", Value: "disable"}, - {Name: "DB_NAME", Value: common.DatabaseName}, - + env := BuildDatabaseEnv(dbHost) + env = append(env, // API configuration - {Name: "GOTRUE_API_HOST", Value: "0.0.0.0"}, - {Name: "GOTRUE_API_PORT", Value: fmt.Sprintf("%d", AuthPort)}, - {Name: "API_EXTERNAL_URL", Value: spec.ExternalURL}, - {Name: "GOTRUE_SITE_URL", Value: spec.SiteURL}, - {Name: "GOTRUE_URI_ALLOW_LIST", Value: "*"}, + corev1.EnvVar{Name: "GOTRUE_API_HOST", Value: "0.0.0.0"}, + corev1.EnvVar{Name: "GOTRUE_API_PORT", Value: fmt.Sprintf("%d", AuthPort)}, + corev1.EnvVar{Name: "API_EXTERNAL_URL", Value: spec.ExternalURL}, + corev1.EnvVar{Name: "GOTRUE_SITE_URL", Value: spec.SiteURL}, + corev1.EnvVar{Name: "GOTRUE_URI_ALLOW_LIST", Value: "*"}, // JWT configuration - {Name: "GOTRUE_JWT_DEFAULT_GROUP_NAME", Value: "authenticated"}, - {Name: "GOTRUE_JWT_ADMIN_ROLES", Value: "service_role"}, - {Name: "GOTRUE_JWT_AUD", Value: "authenticated"}, - {Name: "GOTRUE_JWT_EXP", Value: fmt.Sprintf("%d", common.GetAccessTokenExpiration(project))}, + corev1.EnvVar{Name: "GOTRUE_JWT_DEFAULT_GROUP_NAME", Value: "authenticated"}, + corev1.EnvVar{Name: "GOTRUE_JWT_ADMIN_ROLES", Value: "service_role"}, + corev1.EnvVar{Name: "GOTRUE_JWT_AUD", Value: "authenticated"}, + corev1.EnvVar{Name: "GOTRUE_JWT_EXP", Value: fmt.Sprintf("%d", common.GetAccessTokenExpiration(project))}, // Signup configuration - {Name: "GOTRUE_DISABLE_SIGNUP", Value: fmt.Sprintf("%t", spec.DisableSignup)}, + corev1.EnvVar{Name: "GOTRUE_DISABLE_SIGNUP", Value: fmt.Sprintf("%t", spec.DisableSignup)}, // Email configuration - {Name: "GOTRUE_EXTERNAL_EMAIL_ENABLED", Value: "true"}, - {Name: "GOTRUE_MAILER_AUTOCONFIRM", Value: fmt.Sprintf("%t", spec.AutoConfirmEmail)}, - {Name: "GOTRUE_MAILER_OTP_EXP", Value: "3600"}, + corev1.EnvVar{Name: "GOTRUE_EXTERNAL_EMAIL_ENABLED", Value: "true"}, + corev1.EnvVar{Name: "GOTRUE_MAILER_AUTOCONFIRM", Value: fmt.Sprintf("%t", spec.AutoConfirmEmail)}, + corev1.EnvVar{Name: "GOTRUE_MAILER_OTP_EXP", Value: "3600"}, // Database credentials from secret - { + corev1.EnvVar{ Name: "DB_USER", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ @@ -201,7 +162,7 @@ func buildAuthEnv(project *supabasev1alpha1.SupabaseProject, secretNames *supaba }, }, }, - { + corev1.EnvVar{ Name: "DB_PASSWORD", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ @@ -212,7 +173,7 @@ func buildAuthEnv(project *supabasev1alpha1.SupabaseProject, secretNames *supaba }, }, }, - { + corev1.EnvVar{ Name: "DB_PASSWORD_ENC", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ @@ -225,7 +186,7 @@ func buildAuthEnv(project *supabasev1alpha1.SupabaseProject, secretNames *supaba }, // JWT secret - { + corev1.EnvVar{ Name: "GOTRUE_JWT_SECRET", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ @@ -238,12 +199,12 @@ func buildAuthEnv(project *supabasev1alpha1.SupabaseProject, secretNames *supaba }, // Database URL (constructed from other env vars) - { + corev1.EnvVar{ Name: "GOTRUE_DB_DATABASE_URL", Value: "$(DB_DRIVER)://$(DB_USER):$(DB_PASSWORD_ENC)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?search_path=auth&sslmode=$(DB_SSL)", }, - {Name: "GOTRUE_DB_DRIVER", Value: "$(DB_DRIVER)"}, - } + corev1.EnvVar{Name: "GOTRUE_DB_DRIVER", Value: "$(DB_DRIVER)"}, + ) // Add OAuth provider configuration if spec.Providers != nil { diff --git a/internal/resources/deployments/helpers.go b/internal/resources/deployments/helpers.go new file mode 100644 index 0000000..1135ac8 --- /dev/null +++ b/internal/resources/deployments/helpers.go @@ -0,0 +1,114 @@ +/* +Copyright 2026 GuionAI. + +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 deployments + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + supabasev1alpha1 "github.com/GuionAI/cloudnative-supabase/api/v1alpha1" + "github.com/GuionAI/cloudnative-supabase/internal/resources/common" +) + +// ProbeConfig holds configuration for building HTTP probes. +// All time-related fields are in seconds. +type ProbeConfig struct { + // Path is the HTTP endpoint to probe (e.g., "/health", "/ready") + Path string + // Port is the container port to probe (must be > 0) + Port int32 + // InitialDelaySeconds is how long to wait before first probe (typically 5-30s) + InitialDelaySeconds int32 + // PeriodSeconds is how often to perform the probe (typically 5-15s) + PeriodSeconds int32 + // TimeoutSeconds is how long to wait for probe response (typically 1-5s) + TimeoutSeconds int32 +} + +// DefaultLivenessConfig returns default liveness probe settings +func DefaultLivenessConfig(path string, port int32) ProbeConfig { + return ProbeConfig{ + Path: path, + Port: port, + InitialDelaySeconds: 10, + PeriodSeconds: 10, + TimeoutSeconds: 5, + } +} + +// DefaultReadinessConfig returns default readiness probe settings +func DefaultReadinessConfig(path string, port int32) ProbeConfig { + return ProbeConfig{ + Path: path, + Port: port, + InitialDelaySeconds: 5, + PeriodSeconds: 5, + TimeoutSeconds: 3, + } +} + +// BuildHTTPProbe creates an HTTP probe with the given configuration +func BuildHTTPProbe(config ProbeConfig) *corev1.Probe { + return &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: config.Path, + Port: intstr.FromInt32(config.Port), + }, + }, + InitialDelaySeconds: config.InitialDelaySeconds, + PeriodSeconds: config.PeriodSeconds, + TimeoutSeconds: config.TimeoutSeconds, + FailureThreshold: 3, + } +} + +// BuildLivenessProbe creates a liveness probe with default settings +func BuildLivenessProbe(path string, port int32) *corev1.Probe { + return BuildHTTPProbe(DefaultLivenessConfig(path, port)) +} + +// BuildReadinessProbe creates a readiness probe with default settings +func BuildReadinessProbe(path string, port int32) *corev1.Probe { + return BuildHTTPProbe(DefaultReadinessConfig(path, port)) +} + +// NormalizeReplicas returns 1 if replicas is 0, otherwise returns replicas +func NormalizeReplicas(replicas int32) int32 { + if replicas == 0 { + return 1 + } + return replicas +} + +// AddImagePullSecrets adds image pull secrets to the pod spec if configured +func AddImagePullSecrets(podSpec *corev1.PodSpec, project *supabasev1alpha1.SupabaseProject) { + if len(project.Spec.ImagePullSecrets) > 0 { + podSpec.ImagePullSecrets = project.Spec.ImagePullSecrets + } +} + +// BuildDatabaseEnv creates the common database environment variables used by Auth and Rest +func BuildDatabaseEnv(dbHost string) []corev1.EnvVar { + return []corev1.EnvVar{ + {Name: "DB_HOST", Value: dbHost}, + {Name: "DB_PORT", Value: "5432"}, + {Name: "DB_DRIVER", Value: "postgres"}, + {Name: "DB_SSL", Value: "disable"}, + {Name: "DB_NAME", Value: common.DatabaseName}, + } +} diff --git a/internal/resources/deployments/kong.go b/internal/resources/deployments/kong.go index 3b225df..bf86de6 100644 --- a/internal/resources/deployments/kong.go +++ b/internal/resources/deployments/kong.go @@ -22,7 +22,6 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" supabasev1alpha1 "github.com/GuionAI/cloudnative-supabase/api/v1alpha1" "github.com/GuionAI/cloudnative-supabase/internal/resources/common" @@ -48,11 +47,6 @@ func KongDeploymentName(project *supabasev1alpha1.SupabaseProject) string { return project.Name + "-kong" } -// KongConfigMapName returns the kong config map name -func KongConfigMapName(project *supabasev1alpha1.SupabaseProject) string { - return project.Name + "-kong-config" -} - // BuildKongDeployment creates the Kong API gateway deployment func BuildKongDeployment(project *supabasev1alpha1.SupabaseProject, secretNames *supabasev1alpha1.SecretNamesStatus) *appsv1.Deployment { spec := &project.Spec.Kong @@ -64,10 +58,7 @@ func BuildKongDeployment(project *supabasev1alpha1.SupabaseProject, secretNames imageTag = spec.ImageTag } - replicas := spec.Replicas - if replicas == 0 { - replicas = 1 - } + replicas := NormalizeReplicas(spec.Replicas) env := []corev1.EnvVar{ // Kong configuration @@ -115,12 +106,10 @@ func BuildKongDeployment(project *supabasev1alpha1.SupabaseProject, secretNames deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: project.Namespace, - Labels: common.ComponentLabels(project, KongComponentName), - Annotations: map[string]string{ - "reloader.stakater.com/auto": "true", - }, + Name: name, + Namespace: project.Namespace, + Labels: common.ComponentLabels(project, KongComponentName), + Annotations: common.ReloaderAnnotations(), }, Spec: appsv1.DeploymentSpec{ Replicas: &replicas, @@ -129,10 +118,8 @@ func BuildKongDeployment(project *supabasev1alpha1.SupabaseProject, secretNames }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: common.ComponentLabels(project, KongComponentName), - Annotations: map[string]string{ - "reloader.stakater.com/auto": "true", - }, + Labels: common.ComponentLabels(project, KongComponentName), + Annotations: common.ReloaderAnnotations(), }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ @@ -158,31 +145,15 @@ func BuildKongDeployment(project *supabasev1alpha1.SupabaseProject, secretNames Protocol: corev1.ProtocolTCP, }, }, - LivenessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/status", - Port: intstr.FromInt(KongAdminPort), - }, - }, + LivenessProbe: BuildHTTPProbe(ProbeConfig{ + Path: "/status", + Port: KongAdminPort, InitialDelaySeconds: 15, PeriodSeconds: 10, TimeoutSeconds: 5, - FailureThreshold: 3, - }, - ReadinessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/status", - Port: intstr.FromInt(KongAdminPort), - }, - }, - InitialDelaySeconds: 5, - PeriodSeconds: 5, - TimeoutSeconds: 3, - FailureThreshold: 3, - }, - Resources: spec.Resources, + }), + ReadinessProbe: BuildReadinessProbe("/status", KongAdminPort), + Resources: spec.Resources, VolumeMounts: []corev1.VolumeMount{ { Name: "kong-config", @@ -198,7 +169,7 @@ func BuildKongDeployment(project *supabasev1alpha1.SupabaseProject, secretNames VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: KongConfigMapName(project), + Name: common.KongConfigMapName(project), }, }, }, @@ -209,10 +180,7 @@ func BuildKongDeployment(project *supabasev1alpha1.SupabaseProject, secretNames }, } - // Add image pull secrets if specified - if len(project.Spec.ImagePullSecrets) > 0 { - deployment.Spec.Template.Spec.ImagePullSecrets = project.Spec.ImagePullSecrets - } + AddImagePullSecrets(&deployment.Spec.Template.Spec, project) return deployment } diff --git a/internal/resources/deployments/meta.go b/internal/resources/deployments/meta.go index 3267ce9..c12a610 100644 --- a/internal/resources/deployments/meta.go +++ b/internal/resources/deployments/meta.go @@ -22,7 +22,6 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" supabasev1alpha1 "github.com/GuionAI/cloudnative-supabase/api/v1alpha1" "github.com/GuionAI/cloudnative-supabase/internal/resources/cnpg" @@ -55,10 +54,7 @@ func BuildMetaDeployment(project *supabasev1alpha1.SupabaseProject, secretNames imageTag = spec.ImageTag } - replicas := spec.Replicas - if replicas == 0 { - replicas = 1 - } + replicas := NormalizeReplicas(spec.Replicas) env := []corev1.EnvVar{ // Database configuration @@ -97,12 +93,10 @@ func BuildMetaDeployment(project *supabasev1alpha1.SupabaseProject, secretNames deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: project.Namespace, - Labels: common.ComponentLabels(project, MetaComponentName), - Annotations: map[string]string{ - "reloader.stakater.com/auto": "true", - }, + Name: name, + Namespace: project.Namespace, + Labels: common.ComponentLabels(project, MetaComponentName), + Annotations: common.ReloaderAnnotations(), }, Spec: appsv1.DeploymentSpec{ Replicas: &replicas, @@ -111,10 +105,8 @@ func BuildMetaDeployment(project *supabasev1alpha1.SupabaseProject, secretNames }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: common.ComponentLabels(project, MetaComponentName), - Annotations: map[string]string{ - "reloader.stakater.com/auto": "true", - }, + Labels: common.ComponentLabels(project, MetaComponentName), + Annotations: common.ReloaderAnnotations(), }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ @@ -130,31 +122,9 @@ func BuildMetaDeployment(project *supabasev1alpha1.SupabaseProject, secretNames Protocol: corev1.ProtocolTCP, }, }, - LivenessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/health", - Port: intstr.FromInt(MetaPort), - }, - }, - InitialDelaySeconds: 10, - PeriodSeconds: 10, - TimeoutSeconds: 5, - FailureThreshold: 3, - }, - ReadinessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/health", - Port: intstr.FromInt(MetaPort), - }, - }, - InitialDelaySeconds: 5, - PeriodSeconds: 5, - TimeoutSeconds: 3, - FailureThreshold: 3, - }, - Resources: spec.Resources, + LivenessProbe: BuildLivenessProbe("/health", MetaPort), + ReadinessProbe: BuildReadinessProbe("/health", MetaPort), + Resources: spec.Resources, }, }, }, @@ -162,10 +132,7 @@ func BuildMetaDeployment(project *supabasev1alpha1.SupabaseProject, secretNames }, } - // Add image pull secrets if specified - if len(project.Spec.ImagePullSecrets) > 0 { - deployment.Spec.Template.Spec.ImagePullSecrets = project.Spec.ImagePullSecrets - } + AddImagePullSecrets(&deployment.Spec.Template.Spec, project) return deployment } diff --git a/internal/resources/deployments/rest.go b/internal/resources/deployments/rest.go index c722be0..8aa7931 100644 --- a/internal/resources/deployments/rest.go +++ b/internal/resources/deployments/rest.go @@ -23,7 +23,6 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" supabasev1alpha1 "github.com/GuionAI/cloudnative-supabase/api/v1alpha1" "github.com/GuionAI/cloudnative-supabase/internal/resources/cnpg" @@ -62,27 +61,18 @@ func BuildRestDeployment(project *supabasev1alpha1.SupabaseProject, secretNames schemas = spec.Schemas } - replicas := spec.Replicas - if replicas == 0 { - replicas = 1 - } - - env := []corev1.EnvVar{ - // Database configuration - {Name: "DB_HOST", Value: dbHost}, - {Name: "DB_PORT", Value: "5432"}, - {Name: "DB_DRIVER", Value: "postgres"}, - {Name: "DB_SSL", Value: "disable"}, - {Name: "DB_NAME", Value: common.DatabaseName}, + replicas := NormalizeReplicas(spec.Replicas) + env := BuildDatabaseEnv(dbHost) + env = append(env, // PostgREST configuration - {Name: "PGRST_DB_SCHEMAS", Value: strings.Join(schemas, ",")}, - {Name: "PGRST_DB_ANON_ROLE", Value: "anon"}, - {Name: "PGRST_DB_USE_LEGACY_GUCS", Value: "false"}, - {Name: "PGRST_APP_SETTINGS_JWT_EXP", Value: fmt.Sprintf("%d", common.GetAccessTokenExpiration(project))}, + corev1.EnvVar{Name: "PGRST_DB_SCHEMAS", Value: strings.Join(schemas, ",")}, + corev1.EnvVar{Name: "PGRST_DB_ANON_ROLE", Value: "anon"}, + corev1.EnvVar{Name: "PGRST_DB_USE_LEGACY_GUCS", Value: "false"}, + corev1.EnvVar{Name: "PGRST_APP_SETTINGS_JWT_EXP", Value: fmt.Sprintf("%d", common.GetAccessTokenExpiration(project))}, // Database credentials - { + corev1.EnvVar{ Name: "DB_USER", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ @@ -93,7 +83,7 @@ func BuildRestDeployment(project *supabasev1alpha1.SupabaseProject, secretNames }, }, }, - { + corev1.EnvVar{ Name: "DB_PASSWORD", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ @@ -106,7 +96,7 @@ func BuildRestDeployment(project *supabasev1alpha1.SupabaseProject, secretNames }, // JWT secret - { + corev1.EnvVar{ Name: "PGRST_JWT_SECRET", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ @@ -119,20 +109,18 @@ func BuildRestDeployment(project *supabasev1alpha1.SupabaseProject, secretNames }, // Database URI (constructed from env vars) - { + corev1.EnvVar{ Name: "PGRST_DB_URI", Value: "$(DB_DRIVER)://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=$(DB_SSL)", }, - } + ) deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: project.Namespace, - Labels: common.ComponentLabels(project, RestComponentName), - Annotations: map[string]string{ - "reloader.stakater.com/auto": "true", - }, + Name: name, + Namespace: project.Namespace, + Labels: common.ComponentLabels(project, RestComponentName), + Annotations: common.ReloaderAnnotations(), }, Spec: appsv1.DeploymentSpec{ Replicas: &replicas, @@ -141,10 +129,8 @@ func BuildRestDeployment(project *supabasev1alpha1.SupabaseProject, secretNames }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: common.ComponentLabels(project, RestComponentName), - Annotations: map[string]string{ - "reloader.stakater.com/auto": "true", - }, + Labels: common.ComponentLabels(project, RestComponentName), + Annotations: common.ReloaderAnnotations(), }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ @@ -163,31 +149,9 @@ func BuildRestDeployment(project *supabasev1alpha1.SupabaseProject, secretNames Protocol: corev1.ProtocolTCP, }, }, - LivenessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/", - Port: intstr.FromInt(RestPort), - }, - }, - InitialDelaySeconds: 10, - PeriodSeconds: 10, - TimeoutSeconds: 5, - FailureThreshold: 3, - }, - ReadinessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/", - Port: intstr.FromInt(RestPort), - }, - }, - InitialDelaySeconds: 5, - PeriodSeconds: 5, - TimeoutSeconds: 3, - FailureThreshold: 3, - }, - Resources: spec.Resources, + LivenessProbe: BuildLivenessProbe("/", RestPort), + ReadinessProbe: BuildReadinessProbe("/", RestPort), + Resources: spec.Resources, }, }, }, @@ -195,10 +159,7 @@ func BuildRestDeployment(project *supabasev1alpha1.SupabaseProject, secretNames }, } - // Add image pull secrets if specified - if len(project.Spec.ImagePullSecrets) > 0 { - deployment.Spec.Template.Spec.ImagePullSecrets = project.Spec.ImagePullSecrets - } + AddImagePullSecrets(&deployment.Spec.Template.Spec, project) return deployment } diff --git a/internal/resources/deployments/studio.go b/internal/resources/deployments/studio.go index 59a23f8..53b0f6d 100644 --- a/internal/resources/deployments/studio.go +++ b/internal/resources/deployments/studio.go @@ -22,7 +22,6 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" supabasev1alpha1 "github.com/GuionAI/cloudnative-supabase/api/v1alpha1" "github.com/GuionAI/cloudnative-supabase/internal/resources/cnpg" @@ -65,10 +64,7 @@ func BuildStudioDeployment(project *supabasev1alpha1.SupabaseProject, secretName projName = spec.ProjectName } - replicas := spec.Replicas - if replicas == 0 { - replicas = 1 - } + replicas := NormalizeReplicas(spec.Replicas) // Build internal service URLs kongService := project.Name + "-kong" @@ -157,12 +153,10 @@ func BuildStudioDeployment(project *supabasev1alpha1.SupabaseProject, secretName deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: project.Namespace, - Labels: common.ComponentLabels(project, StudioComponentName), - Annotations: map[string]string{ - "reloader.stakater.com/auto": "true", - }, + Name: name, + Namespace: project.Namespace, + Labels: common.ComponentLabels(project, StudioComponentName), + Annotations: common.ReloaderAnnotations(), }, Spec: appsv1.DeploymentSpec{ Replicas: &replicas, @@ -171,10 +165,8 @@ func BuildStudioDeployment(project *supabasev1alpha1.SupabaseProject, secretName }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: common.ComponentLabels(project, StudioComponentName), - Annotations: map[string]string{ - "reloader.stakater.com/auto": "true", - }, + Labels: common.ComponentLabels(project, StudioComponentName), + Annotations: common.ReloaderAnnotations(), }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ @@ -190,30 +182,20 @@ func BuildStudioDeployment(project *supabasev1alpha1.SupabaseProject, secretName Protocol: corev1.ProtocolTCP, }, }, - LivenessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/api/platform/profile", - Port: intstr.FromInt(StudioPort), - }, - }, + LivenessProbe: BuildHTTPProbe(ProbeConfig{ + Path: "/api/platform/profile", + Port: StudioPort, InitialDelaySeconds: 15, PeriodSeconds: 10, TimeoutSeconds: 10, - FailureThreshold: 3, - }, - ReadinessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/api/platform/profile", - Port: intstr.FromInt(StudioPort), - }, - }, + }), + ReadinessProbe: BuildHTTPProbe(ProbeConfig{ + Path: "/api/platform/profile", + Port: StudioPort, InitialDelaySeconds: 10, PeriodSeconds: 5, TimeoutSeconds: 10, - FailureThreshold: 3, - }, + }), Resources: spec.Resources, }, }, @@ -222,10 +204,7 @@ func BuildStudioDeployment(project *supabasev1alpha1.SupabaseProject, secretName }, } - // Add image pull secrets if specified - if len(project.Spec.ImagePullSecrets) > 0 { - deployment.Spec.Template.Spec.ImagePullSecrets = project.Spec.ImagePullSecrets - } + AddImagePullSecrets(&deployment.Spec.Template.Spec, project) return deployment }