Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 87 additions & 96 deletions internal/controller/supabaseproject_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -419,137 +447,100 @@ 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
}
return err
}

// 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
Expand Down
8 changes: 8 additions & 0 deletions internal/resources/common/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
26 changes: 26 additions & 0 deletions internal/resources/common/names.go
Original file line number Diff line number Diff line change
@@ -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"
}
7 changes: 1 addition & 6 deletions internal/resources/configmaps/kong.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
},
Expand Down
Loading