package controller import ( "context" "encoding/hex" "fmt" "net/url" "strings" "time" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1" "code.icb4dc0.de/prskr/supabase-operator/internal/meta" "code.icb4dc0.de/prskr/supabase-operator/internal/supabase" ) type CorePostgrestReconiler struct { client.Client Scheme *runtime.Scheme } func (r *CorePostgrestReconiler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { var ( core supabasev1alpha1.Core logger = log.FromContext(ctx) ) if err := r.Get(ctx, req.NamespacedName, &core); client.IgnoreNotFound(err) != nil { logger.Error(err, "unable to fetch Core") return ctrl.Result{}, err } if err := r.reconilePostgrestDeployment(ctx, &core); err != nil { if client.IgnoreNotFound(err) == nil { logger.Error(err, "expected resource does not exist (yet), waiting for it to be present") return ctrl.Result{RequeueAfter: 5 * time.Second}, nil } return ctrl.Result{}, err } if err := r.reconcilePostgrestService(ctx, &core); err != nil { return ctrl.Result{}, err } return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *CorePostgrestReconiler) SetupWithManager(mgr ctrl.Manager) error { // TODO watch changes in DB credentials secret return ctrl.NewControllerManagedBy(mgr). For(new(supabasev1alpha1.Core)). Owns(new(appsv1.Deployment)). Owns(new(corev1.Service)). Named("core-postgrest"). Complete(r) } func (r *CorePostgrestReconiler) reconilePostgrestDeployment( ctx context.Context, core *supabasev1alpha1.Core, ) error { var ( serviceCfg = supabase.ServiceConfig.Postgrest postgrestDeployment = &appsv1.Deployment{ ObjectMeta: serviceCfg.ObjectMeta(core), } postgrestSpec = core.Spec.Postgrest ) if postgrestSpec.WorkloadTemplate == nil { postgrestSpec.WorkloadTemplate = new(supabasev1alpha1.WorkloadTemplate) } if postgrestSpec.WorkloadTemplate.Workload == nil { postgrestSpec.WorkloadTemplate.Workload = new(supabasev1alpha1.ContainerTemplate) } var ( image = supabase.Images.Postgrest.String() podSecurityContext = postgrestSpec.WorkloadTemplate.SecurityContext pullPolicy = postgrestSpec.WorkloadTemplate.Workload.PullPolicy containerSecurityContext = postgrestSpec.WorkloadTemplate.Workload.SecurityContext anonRole = ValueOrFallback(postgrestSpec.AnonRole, serviceCfg.Defaults.AnonRole) postgrestSchemas = ValueOrFallback(postgrestSpec.Schemas, serviceCfg.Defaults.Schemas) jwtSecretHash string namespacedClient = client.NewNamespacedClient(r.Client, core.Namespace) ) if img := postgrestSpec.WorkloadTemplate.Workload.Image; img != "" { image = img } if podSecurityContext == nil { podSecurityContext = &corev1.PodSecurityContext{ RunAsNonRoot: ptrOf(true), } } if containerSecurityContext == nil { containerSecurityContext = &corev1.SecurityContext{ Privileged: ptrOf(false), RunAsUser: ptrOf(int64(1000)), RunAsGroup: ptrOf(int64(1000)), RunAsNonRoot: ptrOf(true), AllowPrivilegeEscalation: ptrOf(false), ReadOnlyRootFilesystem: ptrOf(true), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{ "ALL", }, }, } } databaseDSN, err := core.Spec.Database.GetDSN(ctx, namespacedClient) if err != nil { return err } parsedDSN, err := url.Parse(databaseDSN) if err != nil { return fmt.Errorf("failed to parse DB DSN: %w", err) } if jwtSecret, err := core.Spec.JWT.GetJWTSecret(ctx, namespacedClient); err != nil { return err } else { jwtSecretHash = hex.EncodeToString(HashBytes(jwtSecret)) } _, err = controllerutil.CreateOrUpdate(ctx, r.Client, postgrestDeployment, func() error { postgrestDeployment.Labels = MergeLabels( objectLabels(core, serviceCfg.Name, "core", supabase.Images.Postgrest.Tag), core.Labels, ) postgrestEnv := []corev1.EnvVar{ { Name: "DB_CREDENTIALS_PASSWORD", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: core.Spec.Database.Roles.Secrets.Authenticator.Name, }, Key: corev1.BasicAuthPasswordKey, }, }, }, { Name: serviceCfg.EnvKeys.DBUri, Value: strings.TrimSuffix(fmt.Sprintf("postgres://%s:$(DB_CREDENTIALS_PASSWORD)@%s%s?%s", supabase.DBRoleAuthenticator, parsedDSN.Host, parsedDSN.Path, parsedDSN.Query().Encode()), "?"), }, serviceCfg.EnvKeys.JWTSecret.Var(core.Spec.JWT.JwksKeySelector()), serviceCfg.EnvKeys.Schemas.Var(postgrestSchemas), serviceCfg.EnvKeys.AnonRole.Var(anonRole), serviceCfg.EnvKeys.UseLegacyGucs.Var(false), serviceCfg.EnvKeys.ExtraSearchPath.Var(serviceCfg.Defaults.ExtraSearchPath), serviceCfg.EnvKeys.AppSettingsJWTSecret.Var(core.Spec.JWT.SecretKeySelector()), serviceCfg.EnvKeys.AppSettingsJWTExpiry.Var(ValueOrFallback(core.Spec.JWT.Expiry, supabase.ServiceConfig.JWT.Defaults.Expiry)), serviceCfg.EnvKeys.AdminServerPort.Var(3001), } if postgrestDeployment.CreationTimestamp.IsZero() { postgrestDeployment.Spec.Selector = &metav1.LabelSelector{ MatchLabels: selectorLabels(core, serviceCfg.Name), } } postgrestDeployment.Spec.Replicas = postgrestSpec.WorkloadTemplate.Replicas postgrestDeployment.Spec.Template = corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ fmt.Sprintf("%s/%s", supabasev1alpha1.GroupVersion.Group, "jwt-hash"): jwtSecretHash, }, Labels: objectLabels(core, serviceCfg.Name, "core", supabase.Images.Postgrest.Tag), }, Spec: corev1.PodSpec{ ImagePullSecrets: postgrestSpec.WorkloadTemplate.Workload.ImagePullSecrets, Containers: []corev1.Container{ { Name: "supabase-rest", Image: image, ImagePullPolicy: pullPolicy, Args: []string{"postgrest"}, Env: MergeEnv(postgrestEnv, postgrestSpec.WorkloadTemplate.Workload.AdditionalEnv...), Ports: []corev1.ContainerPort{ { Name: "rest", ContainerPort: 3000, Protocol: corev1.ProtocolTCP, }, { Name: "admin", ContainerPort: 3001, Protocol: corev1.ProtocolTCP, }, }, SecurityContext: containerSecurityContext, Resources: postgrestSpec.WorkloadTemplate.Workload.Resources, VolumeMounts: postgrestSpec.WorkloadTemplate.Workload.VolumeMounts, ReadinessProbe: &corev1.Probe{ InitialDelaySeconds: 5, PeriodSeconds: 3, TimeoutSeconds: 1, SuccessThreshold: 2, ProbeHandler: corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ Path: "/ready", Port: intstr.IntOrString{IntVal: 3001}, }, }, }, LivenessProbe: &corev1.Probe{ InitialDelaySeconds: 10, PeriodSeconds: 5, TimeoutSeconds: 3, ProbeHandler: corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ Path: "/live", Port: intstr.IntOrString{IntVal: 3001}, }, }, }, }, }, SecurityContext: podSecurityContext, }, } if err := controllerutil.SetControllerReference(core, postgrestDeployment, r.Scheme); err != nil { return err } return nil }) return err } func (r *CorePostgrestReconiler) reconcilePostgrestService( ctx context.Context, core *supabasev1alpha1.Core, ) error { postgrestService := &corev1.Service{ ObjectMeta: supabase.ServiceConfig.Postgrest.ObjectMeta(core), } _, err := controllerutil.CreateOrUpdate(ctx, r.Client, postgrestService, func() error { postgrestService.Labels = MergeLabels( objectLabels(core, supabase.ServiceConfig.Postgrest.Name, "core", supabase.Images.Postgrest.Tag), core.Labels, ) postgrestService.Labels[meta.SupabaseLabel.EnvoyCluster] = core.Name postgrestService.Spec = corev1.ServiceSpec{ Selector: selectorLabels(core, supabase.ServiceConfig.Postgrest.Name), Ports: []corev1.ServicePort{ { Name: "rest", Protocol: corev1.ProtocolTCP, AppProtocol: ptrOf("http"), Port: 3000, TargetPort: intstr.IntOrString{IntVal: 3000}, }, }, } if err := controllerutil.SetControllerReference(core, postgrestService, r.Scheme); err != nil { return err } return nil }) return err }