From c06480ad9e880586a0e20e21f29ea9e26cc1183b Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 5 Sep 2026 11:18:54 +0800 Subject: [PATCH 1/2] fix(powersync): use supported env config syntax --- internal/resources/configmaps/powersync.go | 116 +++++------------- .../resources/configmaps/powersync_test.go | 46 +++++-- internal/resources/deployments/powersync.go | 4 +- .../resources/deployments/powersync_test.go | 5 + 4 files changed, 74 insertions(+), 97 deletions(-) diff --git a/internal/resources/configmaps/powersync.go b/internal/resources/configmaps/powersync.go index 7017ec6..8d8b09d 100644 --- a/internal/resources/configmaps/powersync.go +++ b/internal/resources/configmaps/powersync.go @@ -17,7 +17,7 @@ limitations under the License. package configmaps import ( - "encoding/json" + "fmt" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -41,92 +41,34 @@ func PowersyncSyncRulesConfigMapName(project *supabasev1alpha1.SupabaseProject) return project.Name + "-powersync-sync-rules" } -// powersyncConfig represents the PowerSync service config.json structure -type powersyncConfig struct { - Storage powersyncStorage `json:"storage"` - Replication powersyncReplication `json:"replication"` - Dev powersyncDev `json:"dev"` - ClientAuth powersyncClientAuth `json:"client_auth"` - Migrations powersyncMigrations `json:"migrations"` - Port int `json:"port"` - SyncRules powersyncSyncRules `json:"sync_rules"` - Telemetry powersyncTelemetry `json:"telemetry"` -} - -type powersyncStorage struct { - Type string `json:"type"` - URI string `json:"uri"` -} - -type powersyncReplication struct { - Connections []powersyncConnection `json:"connections"` -} - -type powersyncConnection struct { - Type string `json:"type"` - URI string `json:"uri"` - Tag string `json:"tag"` -} - -type powersyncClientAuth struct { - // Supabase is kept false so PowerSync does not enable its legacy HMAC - // integration. JWTs are verified through the public JWKS URI instead. - Supabase bool `json:"supabase"` - JWKSURI string `json:"jwks_uri"` - Audience []string `json:"audience"` -} - -type powersyncDev struct { - DemoAuth bool `json:"demo_auth"` -} - -type powersyncMigrations struct { - DisableAutoMigration bool `json:"disable_auto_migration"` -} - -type powersyncSyncRules struct { - Path string `json:"path"` - ExitOnError bool `json:"exit_on_error"` -} - -type powersyncTelemetry struct { - DisableTelemetrySharing bool `json:"disable_telemetry_sharing"` -} - -// BuildPowersyncConfigMap creates the PowerSync config.json ConfigMap. -// Database credentials are injected via environment variable templates that -// PowerSync resolves at runtime. +// BuildPowersyncConfigMap creates the PowerSync config.yaml ConfigMap. +// PowerSync's !env tag resolves the database URIs at runtime without putting +// credentials in the ConfigMap. func BuildPowersyncConfigMap(project *supabasev1alpha1.SupabaseProject) *corev1.ConfigMap { - config := powersyncConfig{ - Storage: powersyncStorage{ - Type: "postgresql", - URI: "{{ env.PS_POWERSYNC_STORAGE_URI }}", - }, - Replication: powersyncReplication{ - Connections: []powersyncConnection{ - { - Type: "postgresql", - URI: "{{ env.PS_POWERSYNC_REPLICATION_URI }}", - Tag: "default", - }, - }, - }, - Dev: powersyncDev{DemoAuth: false}, - ClientAuth: powersyncClientAuth{ - Supabase: false, - JWKSURI: common.AuthJWKSURL(project.Spec.Auth.ExternalURL), - Audience: []string{"authenticated"}, - }, - Migrations: powersyncMigrations{DisableAutoMigration: false}, - Port: 8080, - SyncRules: powersyncSyncRules{ - Path: "/powersync/sync_rules/sync_rules.yaml", - ExitOnError: true, - }, - Telemetry: powersyncTelemetry{DisableTelemetrySharing: false}, - } - - configJSON, _ := json.MarshalIndent(config, "", " ") + configYAML := fmt.Sprintf(`storage: + type: postgresql + uri: !env PS_POWERSYNC_STORAGE_URI +replication: + connections: + - type: postgresql + uri: !env PS_POWERSYNC_REPLICATION_URI + tag: default +dev: + demo_auth: false +client_auth: + supabase: false + jwks_uri: %q + audience: + - authenticated +migrations: + disable_auto_migration: false +port: 8080 +sync_rules: + path: /powersync/sync_rules/sync_rules.yaml + exit_on_error: true +telemetry: + disable_telemetry_sharing: false +`, common.AuthJWKSURL(project.Spec.Auth.ExternalURL)) return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -135,7 +77,7 @@ func BuildPowersyncConfigMap(project *supabasev1alpha1.SupabaseProject) *corev1. Labels: common.ComponentLabels(project, PowersyncConfigComponentName), }, Data: map[string]string{ - "config.json": string(configJSON), + "config.yaml": configYAML, }, } } diff --git a/internal/resources/configmaps/powersync_test.go b/internal/resources/configmaps/powersync_test.go index b6dfb74..ecbf8e3 100644 --- a/internal/resources/configmaps/powersync_test.go +++ b/internal/resources/configmaps/powersync_test.go @@ -1,11 +1,11 @@ package configmaps import ( - "encoding/json" "strings" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" supabasev1alpha1 "github.com/GuionAI/cloudnative-supabase/api/v1alpha1" ) @@ -15,6 +15,29 @@ const ( testNamespace = "test-ns" ) +type powersyncConfig struct { + Storage struct { + Type string `json:"type"` + URI string `json:"uri"` + } `json:"storage"` + Replication struct { + Connections []struct { + Type string `json:"type"` + URI string `json:"uri"` + Tag string `json:"tag"` + } `json:"connections"` + } `json:"replication"` + ClientAuth struct { + Supabase bool `json:"supabase"` + JWKSURI string `json:"jwks_uri"` + Audience []string `json:"audience"` + } `json:"client_auth"` + SyncRules struct { + Path string `json:"path"` + ExitOnError bool `json:"exit_on_error"` + } `json:"sync_rules"` +} + func newTestProject(namespace string) *supabasev1alpha1.SupabaseProject { return &supabasev1alpha1.SupabaseProject{ ObjectMeta: metav1.ObjectMeta{ @@ -89,24 +112,28 @@ func TestBuildPowersyncConfigMap(t *testing.T) { t.Errorf("Namespace = %q, want %q", cm.Namespace, testNamespace) } - configJSON, ok := cm.Data["config.json"] + configYAML, ok := cm.Data["config.yaml"] if !ok { - t.Fatal("config.json key not found") + t.Fatal("config.yaml key not found") } - // Parse the JSON to validate structure + // Parse the YAML to validate structure. The raw assertions below preserve + // the !env tags that PowerSync resolves before decoding the values. var config powersyncConfig - if err := json.Unmarshal([]byte(configJSON), &config); err != nil { - t.Fatalf("invalid JSON: %v", err) + if err := yaml.Unmarshal([]byte(configYAML), &config); err != nil { + t.Fatalf("invalid YAML: %v", err) } // Storage if config.Storage.Type != "postgresql" { t.Errorf("storage type = %q, want postgresql", config.Storage.Type) } - if config.Storage.URI != "{{ env.PS_POWERSYNC_STORAGE_URI }}" { + if config.Storage.URI != "PS_POWERSYNC_STORAGE_URI" { t.Errorf("storage URI = %q, want environment template", config.Storage.URI) } + if !strings.Contains(configYAML, "uri: !env PS_POWERSYNC_STORAGE_URI") { + t.Error("storage URI must use PowerSync's !env tag") + } // Replication if len(config.Replication.Connections) != 1 { @@ -119,9 +146,12 @@ func TestBuildPowersyncConfigMap(t *testing.T) { if conn.Tag != "default" { t.Errorf("connection tag = %q, want default", conn.Tag) } - if conn.URI != "{{ env.PS_POWERSYNC_REPLICATION_URI }}" { + if conn.URI != "PS_POWERSYNC_REPLICATION_URI" { t.Errorf("replication URI = %q, want environment template", conn.URI) } + if !strings.Contains(configYAML, "uri: !env PS_POWERSYNC_REPLICATION_URI") { + t.Error("replication URI must use PowerSync's !env tag") + } // Client auth if config.ClientAuth.Supabase { diff --git a/internal/resources/deployments/powersync.go b/internal/resources/deployments/powersync.go index 5a04eef..44c327c 100644 --- a/internal/resources/deployments/powersync.go +++ b/internal/resources/deployments/powersync.go @@ -339,7 +339,7 @@ func buildPowersyncEnv(project *supabasev1alpha1.SupabaseProject, secretNames *s dbHost := cnpg.ClusterRWServiceName(project) return []corev1.EnvVar{ - {Name: "POWERSYNC_CONFIG_PATH", Value: "/powersync/config/config.json"}, + {Name: "POWERSYNC_CONFIG_PATH", Value: "/powersync/config/config.yaml"}, {Name: "NODE_OPTIONS", Value: nodeOptions}, {Name: "LOG_FORMAT", Value: "json"}, {Name: "METRICS_PORT", Value: "9464"}, @@ -370,7 +370,7 @@ func buildPowersyncEnv(project *supabasev1alpha1.SupabaseProject, secretNames *s }, }, }, - // PowerSync resolves {{ env.VAR }} in config.json + // PowerSync resolves !env PS_* tags in config.yaml. { Name: "PS_POWERSYNC_STORAGE_URI", Value: fmt.Sprintf("postgresql://powersync_storage:$(PS_STORAGE_PASSWORD)@%s:5432/supabase?sslmode=disable", dbHost), diff --git a/internal/resources/deployments/powersync_test.go b/internal/resources/deployments/powersync_test.go index 1cbc023..66d7e93 100644 --- a/internal/resources/deployments/powersync_test.go +++ b/internal/resources/deployments/powersync_test.go @@ -295,4 +295,9 @@ func TestBuildPowersyncEnvVars(t *testing.T) { t.Errorf("missing required env var: %s", name) } } + for _, env := range env { + if env.Name == "POWERSYNC_CONFIG_PATH" && env.Value != "/powersync/config/config.yaml" { + t.Errorf("POWERSYNC_CONFIG_PATH = %q, want config.yaml", env.Value) + } + } } From 8d7506305eb56c1650686d0b2a1290cd6116e579 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 5 Sep 2026 11:57:03 +0800 Subject: [PATCH 2/2] fix(powersync): hash rendered yaml config --- internal/controller/powersync_lifecycle_test.go | 5 +++++ internal/controller/supabaseproject_controller.go | 4 ++-- internal/resources/configmaps/powersync.go | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/controller/powersync_lifecycle_test.go b/internal/controller/powersync_lifecycle_test.go index b33037e..8c8b36f 100644 --- a/internal/controller/powersync_lifecycle_test.go +++ b/internal/controller/powersync_lifecycle_test.go @@ -243,6 +243,11 @@ func TestApplyPowerSyncConfigHashChangesPodTemplate(t *testing.T) { if hashB := deploymentB.Spec.Template.Annotations[powerSyncConfigHashAnnotation]; hashB == hashA { t.Fatal("sync rule changes must change the pod template hash") } + deploymentC := &appsv1.Deployment{} + applyPowerSyncConfigHash(deploymentC, "changed-config", []byte("rules-a")) + if hashC := deploymentC.Spec.Template.Annotations[powerSyncConfigHashAnnotation]; hashC == hashA { + t.Fatal("generated config changes must change the pod template hash") + } } func TestMapExternalPowerSyncConfigMapToProjects(t *testing.T) { diff --git a/internal/controller/supabaseproject_controller.go b/internal/controller/supabaseproject_controller.go index 1254c85..d27af5b 100644 --- a/internal/controller/supabaseproject_controller.go +++ b/internal/controller/supabaseproject_controller.go @@ -2039,7 +2039,7 @@ func (r *SupabaseProjectReconciler) reconcilePowersync(ctx context.Context, proj // Deploy Powersync API apiDeployment := deployments.BuildPowersyncAPIDeployment(project, secretNames) - applyPowerSyncConfigHash(apiDeployment, psConfig.Data["config.json"], syncRulesContent) + applyPowerSyncConfigHash(apiDeployment, psConfig.Data[configmaps.PowersyncConfigKey], syncRulesContent) if err := r.createOrUpdateDeployment(ctx, project, apiDeployment); err != nil { r.setCondition(project, supabasev1alpha1.ConditionTypePowersyncReady, metav1.ConditionFalse, "APIDeploymentFailed", err.Error()) if statusErr := r.updateProjectStatus(ctx, project); statusErr != nil { @@ -2060,7 +2060,7 @@ func (r *SupabaseProjectReconciler) reconcilePowersync(ctx context.Context, proj // Deploy Powersync Replication replDeployment := deployments.BuildPowersyncReplicationDeployment(project, secretNames) - applyPowerSyncConfigHash(replDeployment, psConfig.Data["config.json"], syncRulesContent) + applyPowerSyncConfigHash(replDeployment, psConfig.Data[configmaps.PowersyncConfigKey], syncRulesContent) if err := r.createOrUpdateDeployment(ctx, project, replDeployment); err != nil { r.setCondition(project, supabasev1alpha1.ConditionTypePowersyncReady, metav1.ConditionFalse, "ReplicationDeploymentFailed", err.Error()) if statusErr := r.updateProjectStatus(ctx, project); statusErr != nil { diff --git a/internal/resources/configmaps/powersync.go b/internal/resources/configmaps/powersync.go index 8d8b09d..d4b8ef3 100644 --- a/internal/resources/configmaps/powersync.go +++ b/internal/resources/configmaps/powersync.go @@ -29,6 +29,7 @@ import ( const ( PowersyncConfigComponentName = "powersync-config" PowersyncSyncRulesComponentName = "powersync-sync-rules" + PowersyncConfigKey = "config.yaml" ) // PowersyncConfigMapName returns the Powersync config ConfigMap name @@ -77,7 +78,7 @@ telemetry: Labels: common.ComponentLabels(project, PowersyncConfigComponentName), }, Data: map[string]string{ - "config.yaml": configYAML, + PowersyncConfigKey: configYAML, }, } }