From dd7dfb8550b0626078295d9b290c1ebccafdc191 Mon Sep 17 00:00:00 2001 From: STRRL <20221408+STRRL@users.noreply.github.com> Date: Thu, 23 Jul 2026 06:25:01 +0000 Subject: [PATCH 1/4] chore: sync component image versions from upstream Supabase --- api/v1alpha1/supabaseproject_types.go | 14 +++++++------- api/v1alpha1/wellknown_images.go | 14 +++++++------- .../crds/supabase.strrl.dev_supabaseprojects.yaml | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/api/v1alpha1/supabaseproject_types.go b/api/v1alpha1/supabaseproject_types.go index d062071..9b0e496 100644 --- a/api/v1alpha1/supabaseproject_types.go +++ b/api/v1alpha1/supabaseproject_types.go @@ -99,7 +99,7 @@ type StorageConfig struct { } type KongConfig struct { - // +kubebuilder:default="kong:2.8.1" + // +kubebuilder:default="kong/kong:3.9.1" // +optional Image string `json:"image,omitempty"` @@ -117,7 +117,7 @@ type KongConfig struct { } type AuthConfig struct { - // +kubebuilder:default="supabase/gotrue:v2.180.0" + // +kubebuilder:default="supabase/gotrue:v2.189.0" // +optional Image string `json:"image,omitempty"` @@ -141,7 +141,7 @@ type AuthConfig struct { } type RealtimeConfig struct { - // +kubebuilder:default="supabase/realtime:v2.51.11" + // +kubebuilder:default="supabase/realtime:v2.102.3" // +optional Image string `json:"image,omitempty"` @@ -159,7 +159,7 @@ type RealtimeConfig struct { } type PostgRESTConfig struct { - // +kubebuilder:default="postgrest/postgrest:v13.0.7" + // +kubebuilder:default="postgrest/postgrest:v14.12" // +optional Image string `json:"image,omitempty"` @@ -177,7 +177,7 @@ type PostgRESTConfig struct { } type StorageAPIConfig struct { - // +kubebuilder:default="supabase/storage-api:v1.32.0" + // +kubebuilder:default="supabase/storage-api:v1.60.4" // +optional Image string `json:"image,omitempty"` @@ -195,7 +195,7 @@ type StorageAPIConfig struct { } type MetaConfig struct { - // +kubebuilder:default="supabase/postgres-meta:v0.93.1" + // +kubebuilder:default="supabase/postgres-meta:v0.96.6" // +optional Image string `json:"image,omitempty"` @@ -213,7 +213,7 @@ type MetaConfig struct { } type StudioConfig struct { - // +kubebuilder:default="supabase/studio:2025.10.01-sha-8460121" + // +kubebuilder:default="supabase/studio:2026.07.07-sha-a6a04f2" // +optional Image string `json:"image,omitempty"` diff --git a/api/v1alpha1/wellknown_images.go b/api/v1alpha1/wellknown_images.go index efea960..9e2f923 100644 --- a/api/v1alpha1/wellknown_images.go +++ b/api/v1alpha1/wellknown_images.go @@ -9,13 +9,13 @@ package v1alpha1 // which reads the upstream compose file: // https://github.com/supabase/supabase/blob/master/docker/docker-compose.yml const ( - DefaultKongImage = "kong:2.8.1" - DefaultAuthImage = "supabase/gotrue:v2.180.0" - DefaultPostgRESTImage = "postgrest/postgrest:v13.0.7" - DefaultRealtimeImage = "supabase/realtime:v2.51.11" - DefaultStorageAPIImage = "supabase/storage-api:v1.32.0" - DefaultMetaImage = "supabase/postgres-meta:v0.93.1" - DefaultStudioImage = "supabase/studio:2025.10.01-sha-8460121" + DefaultKongImage = "kong/kong:3.9.1" + DefaultAuthImage = "supabase/gotrue:v2.189.0" + DefaultPostgRESTImage = "postgrest/postgrest:v14.12" + DefaultRealtimeImage = "supabase/realtime:v2.102.3" + DefaultStorageAPIImage = "supabase/storage-api:v1.60.4" + DefaultMetaImage = "supabase/postgres-meta:v0.96.6" + DefaultStudioImage = "supabase/studio:2026.07.07-sha-a6a04f2" ) // DefaultPostgresImage is used by the database init job only. The operator diff --git a/helm/supabase-operator/crds/supabase.strrl.dev_supabaseprojects.yaml b/helm/supabase-operator/crds/supabase.strrl.dev_supabaseprojects.yaml index 2fbcd7a..5b60ffa 100644 --- a/helm/supabase-operator/crds/supabase.strrl.dev_supabaseprojects.yaml +++ b/helm/supabase-operator/crds/supabase.strrl.dev_supabaseprojects.yaml @@ -199,7 +199,7 @@ spec: type: object type: array image: - default: supabase/gotrue:v2.180.0 + default: supabase/gotrue:v2.189.0 type: string oauthSecretRef: description: |- @@ -519,7 +519,7 @@ spec: type: object type: array image: - default: kong:2.8.1 + default: kong/kong:3.9.1 type: string replicas: default: 1 @@ -748,7 +748,7 @@ spec: type: object type: array image: - default: supabase/postgres-meta:v0.93.1 + default: supabase/postgres-meta:v0.96.6 type: string replicas: default: 1 @@ -977,7 +977,7 @@ spec: type: object type: array image: - default: postgrest/postgrest:v13.0.7 + default: postgrest/postgrest:v14.12 type: string replicas: default: 1 @@ -1209,7 +1209,7 @@ spec: type: object type: array image: - default: supabase/realtime:v2.51.11 + default: supabase/realtime:v2.102.3 type: string replicas: default: 1 @@ -1470,7 +1470,7 @@ spec: type: object type: array image: - default: supabase/storage-api:v1.32.0 + default: supabase/storage-api:v1.60.4 type: string replicas: default: 1 @@ -1716,7 +1716,7 @@ spec: type: object type: array image: - default: supabase/studio:2025.10.01-sha-8460121 + default: supabase/studio:2026.07.07-sha-a6a04f2 type: string publicUrl: type: string From 98bf09e4188ea8b50a4582250456395ea23f7eb7 Mon Sep 17 00:00:00 2001 From: Zhiqiang ZHOU Date: Fri, 24 Jul 2026 20:11:47 -0700 Subject: [PATCH 2/4] fix: align component configs with upstream Supabase major version upgrades Kong 2.8 -> 3.9: the kong/kong image renamed /docker-entrypoint.sh to /entrypoint.sh, causing the container to crashloop. Replace the inline eval/echo command with an entrypoint script shipped in the ConfigMap that mirrors upstream kong-entrypoint.sh (awk-based env substitution), write the rendered config to /usr/local/kong/kong.yml (writable by the kong user), align the declarative config and env vars with upstream kong.yml, and add kong health probes. Realtime v2.51 -> v2.102: JWT_SECRET is no longer read and METRICS_JWT_SECRET is now required at boot. Switch to API_JWT_SECRET, add METRICS_JWT_SECRET, and add self-host tenant seeding (SEED_SELF_HOST, SELF_HOST_TENANT_NAME matching the Service name, DB_AFTER_CONNECT_QUERY, DB_ENC_KEY, ERL_AFLAGS, DNS_NODES, RUN_JANITOR). Drop the removed SECURE_CHANNELS. storage-api v1.60: rename PGRST_JWT_SECRET to AUTH_JWT_SECRET. PostgREST v14: use PGRST_DB_SCHEMAS (plural). GoTrue v2.189: add GOTRUE_API_HOST, GOTRUE_JWT_ADMIN_ROLES, GOTRUE_JWT_AUD. Studio 2026.07: add POSTGRES_HOST/PORT/DB and feature toggles. --- internal/component/auth.go | 12 + internal/component/kong.go | 663 ++++++++++++------ internal/component/postgrest.go | 2 +- internal/component/realtime.go | 45 +- internal/component/storage.go | 2 +- internal/component/studio.go | 29 + internal/webhook/supabaseproject_webhook.go | 2 +- .../webhook/supabaseproject_webhook_test.go | 4 +- test/e2e/e2e_test.go | 2 +- 9 files changed, 531 insertions(+), 230 deletions(-) diff --git a/internal/component/auth.go b/internal/component/auth.go index 3ac0c1e..f0a46d9 100644 --- a/internal/component/auth.go +++ b/internal/component/auth.go @@ -56,6 +56,10 @@ func (b *AuthBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*appsv Name: "GOTRUE_SITE_URL", Value: "http://localhost:8000", }, + { + Name: "GOTRUE_API_HOST", + Value: "0.0.0.0", + }, { Name: "GOTRUE_API_PORT", Value: "9999", @@ -142,6 +146,14 @@ func (b *AuthBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*appsv Name: "GOTRUE_JWT_EXP", Value: "3600", }, + { + Name: "GOTRUE_JWT_ADMIN_ROLES", + Value: "service_role", + }, + { + Name: "GOTRUE_JWT_AUD", + Value: "authenticated", + }, { Name: "GOTRUE_JWT_DEFAULT_GROUP_NAME", Value: "authenticated", diff --git a/internal/component/kong.go b/internal/component/kong.go index 5ac318c..9463d19 100644 --- a/internal/component/kong.go +++ b/internal/component/kong.go @@ -1,7 +1,6 @@ package component import ( - "fmt" "strings" "github.com/strrl/supabase-operator/api/v1alpha1" @@ -12,6 +11,395 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" ) +// kongDeclarativeConfigPath is where the entrypoint script writes the rendered +// declarative config. It must be writable by the kong user (uid 1001) in the +// official kong/kong image. +const kongDeclarativeConfigPath = "/usr/local/kong/kong.yml" + +// kongEntrypointScript mirrors upstream supabase docker/volumes/api/kong-entrypoint.sh. +// The operator does not support opaque API keys yet, so only the legacy +// passthrough branch of the Lua expressions is kept. Environment variable +// substitution uses awk instead of eval/echo to preserve YAML quoting. +const kongEntrypointScript = `#!/bin/sh +export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or headers.apikey)" +export LUA_RT_WS_EXPR="\$(query_params.apikey)" + +awk '{ + result = "" + rest = $0 + while (match(rest, /\$[A-Za-z_][A-Za-z_0-9]*/)) { + varname = substr(rest, RSTART + 1, RLENGTH - 1) + if (varname in ENVIRON) { + result = result substr(rest, 1, RSTART - 1) ENVIRON[varname] + } else { + result = result substr(rest, 1, RSTART + RLENGTH - 1) + } + rest = substr(rest, RSTART + RLENGTH) + } + print result rest +}' /etc/kong/kong.yml > "$KONG_DECLARATIVE_CONFIG" + +exec /entrypoint.sh kong docker-start +` + +// kongDeclarativeConfigTemplate mirrors upstream supabase docker/volumes/api/kong.yml. +// {{PROJECT}} is replaced with the SupabaseProject name. Routes for components +// the operator does not deploy (edge functions, analytics) are omitted. +const kongDeclarativeConfigTemplate = `_format_version: '2.1' +_transform: true + +consumers: + - username: DASHBOARD + - username: anon + keyauth_credentials: + - key: $SUPABASE_ANON_KEY + - username: service_role + keyauth_credentials: + - key: $SUPABASE_SERVICE_KEY + +acls: + - consumer: anon + group: anon + - consumer: service_role + group: admin + +basicauth_credentials: + - consumer: DASHBOARD + username: '$DASHBOARD_USERNAME' + password: '$DASHBOARD_PASSWORD' + +services: + - name: auth-v1-open + url: http://{{PROJECT}}-auth:9999/verify + routes: + - name: auth-v1-open + strip_path: true + paths: + - /auth/v1/verify + plugins: + - name: cors + + - name: auth-v1-open-callback + url: http://{{PROJECT}}-auth:9999/callback + routes: + - name: auth-v1-open-callback + strip_path: true + paths: + - /auth/v1/callback + plugins: + - name: cors + + - name: auth-v1-open-authorize + url: http://{{PROJECT}}-auth:9999/authorize + routes: + - name: auth-v1-open-authorize + strip_path: true + paths: + - /auth/v1/authorize + plugins: + - name: cors + + - name: auth-v1-open-jwks + url: http://{{PROJECT}}-auth:9999/.well-known/jwks.json + routes: + - name: auth-v1-open-jwks + strip_path: true + paths: + - /auth/v1/.well-known/jwks.json + plugins: + - name: cors + + - name: auth-v1-open-sso-acs + url: http://{{PROJECT}}-auth:9999/sso/saml/acs + routes: + - name: auth-v1-open-sso-acs + strip_path: true + paths: + - /auth/v1/sso/saml/acs + plugins: + - name: cors + + - name: auth-v1-open-sso-metadata + url: http://{{PROJECT}}-auth:9999/sso/saml/metadata + routes: + - name: auth-v1-open-sso-metadata + strip_path: true + paths: + - /auth/v1/sso/saml/metadata + plugins: + - name: cors + + - name: auth-v1 + url: http://{{PROJECT}}-auth:9999/ + routes: + - name: auth-v1-all + strip_path: true + paths: + - /auth/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + - name: rest-v1-openapi + url: http://{{PROJECT}}-postgrest:3000/ + routes: + - name: rest-v1-openapi-root + strip_path: true + expression: 'http.path == "/rest/v1/"' + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" + - name: acl + config: + hide_groups_header: true + allow: + - admin + + - name: rest-v1 + url: http://{{PROJECT}}-postgrest:3000/ + routes: + - name: rest-v1-all + strip_path: true + paths: + - /rest/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + - name: graphql-v1 + url: http://{{PROJECT}}-postgrest:3000/rpc/graphql + routes: + - name: graphql-v1-all + strip_path: true + paths: + - /graphql/v1 + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Content-Profile: graphql_public" + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + - name: realtime-v1-ws + url: http://{{PROJECT}}-realtime:4000/socket + protocol: ws + routes: + - name: realtime-v1-ws + strip_path: true + paths: + - /realtime/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "x-api-key:$LUA_RT_WS_EXPR" + replace: + querystring: + - "apikey:$LUA_RT_WS_EXPR" + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + - name: realtime-v1-rest-openapi + url: http://{{PROJECT}}-realtime:4000/api/openapi + protocol: http + routes: + - name: realtime-v1-rest-openapi + strip_path: true + paths: + - /realtime/v1/api/openapi + plugins: + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + + - name: realtime-v1-rest-tenants + url: http://{{PROJECT}}-realtime:4000/api/tenants + protocol: http + routes: + - name: realtime-v1-rest-tenants + strip_path: true + paths: + - /realtime/v1/api/tenants + plugins: + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + + - name: realtime-v1-rest + url: http://{{PROJECT}}-realtime:4000/api + protocol: http + routes: + - name: realtime-v1-rest + strip_path: true + paths: + - /realtime/v1/api + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + - name: storage-v1 + url: http://{{PROJECT}}-storage:5000/ + routes: + - name: storage-v1-all + strip_path: true + paths: + - /storage/v1/ + plugins: + - name: cors + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" + - name: post-function + config: + access: + - | + local auth = kong.request.get_header("authorization") + if auth == nil or auth == "" or auth:find("^%s*$") then + kong.service.request.clear_header("authorization") + end + + - name: well-known-oauth + url: http://{{PROJECT}}-auth:9999/.well-known/oauth-authorization-server + routes: + - name: well-known-oauth + strip_path: true + paths: + - /.well-known/oauth-authorization-server + plugins: + - name: cors + + - name: meta + url: http://{{PROJECT}}-meta:8080/ + routes: + - name: meta-all + strip_path: true + paths: + - /pg/ + plugins: + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + + - name: mcp-blocker + url: http://{{PROJECT}}-studio:3000/api/mcp + routes: + - name: mcp-blocker-route + strip_path: true + paths: + - /api/mcp + plugins: + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + + - name: dashboard + url: http://{{PROJECT}}-studio:3000/ + routes: + - name: dashboard-all + strip_path: true + paths: + - / + plugins: + - name: cors + - name: basic-auth + config: + hide_credentials: true +` + type KongBuilder struct{} var _ ComponentBuilder = (*KongBuilder)(nil) @@ -44,8 +432,7 @@ func (b *KongBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*appsv "app.kubernetes.io/managed-by": "supabase-operator", } - plugins := "request-transformer,cors,key-auth,acl,basic-auth" - declConfigPath := "/tmp/kong.yml" + plugins := "request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function" env := []corev1.EnvVar{ { @@ -54,11 +441,15 @@ func (b *KongBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*appsv }, { Name: "KONG_DECLARATIVE_CONFIG", - Value: declConfigPath, + Value: kongDeclarativeConfigPath, + }, + { + Name: "KONG_ROUTER_FLAVOR", + Value: "expressions", }, { Name: "KONG_PROXY_ACCESS_LOG", - Value: "/dev/stdout", + Value: "/dev/stdout combined", }, { Name: "KONG_ADMIN_ACCESS_LOG", @@ -80,10 +471,22 @@ func (b *KongBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*appsv Name: "KONG_DNS_ORDER", Value: "LAST,A,CNAME", }, + { + Name: "KONG_DNS_NOT_FOUND_TTL", + Value: "1", + }, { Name: "KONG_PLUGINS", Value: plugins, }, + { + Name: "KONG_NGINX_PROXY_PROXY_BUFFER_SIZE", + Value: "160k", + }, + { + Name: "KONG_NGINX_PROXY_PROXY_BUFFERS", + Value: "64 160k", + }, } env = append(env, @@ -132,6 +535,18 @@ func (b *KongBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*appsv } env = append(env, usernameEnv, passwordEnv) + healthProbe := &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{"kong", "health"}, + }, + }, + InitialDelaySeconds: 5, + PeriodSeconds: 10, + TimeoutSeconds: 5, + FailureThreshold: 5, + } + deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: project.Name + "-kong", @@ -154,6 +569,22 @@ func (b *KongBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*appsv Image: image, Resources: resources, Env: env, + Command: []string{ + "/bin/sh", + "/etc/kong/kong-entrypoint.sh", + }, + ReadinessProbe: healthProbe, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{"kong", "health"}, + }, + }, + InitialDelaySeconds: 15, + PeriodSeconds: 20, + TimeoutSeconds: 5, + FailureThreshold: 3, + }, Ports: []corev1.ContainerPort{ { Name: "proxy", @@ -197,12 +628,6 @@ func (b *KongBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*appsv }, } - deployment.Spec.Template.Spec.Containers[0].Command = []string{ - "bash", - "-lc", - "eval \"echo \\\"$$(cat /etc/kong/kong.yml)\\\"\" > /tmp/kong.yml && export KONG_DECLARATIVE_CONFIG=/tmp/kong.yml && /docker-entrypoint.sh kong docker-start", - } - if project.Spec.Kong != nil && len(project.Spec.Kong.ExtraEnv) > 0 { deployment.Spec.Template.Spec.Containers[0].Env = append( deployment.Spec.Template.Spec.Containers[0].Env, @@ -222,216 +647,7 @@ func BuildKongConfigMap(project *v1alpha1.SupabaseProject) *corev1.ConfigMap { "app.kubernetes.io/managed-by": "supabase-operator", } - var builder strings.Builder - builder.WriteString(`_format_version: '2.1' -_transform: true - -consumers: - - username: DASHBOARD - - username: anon - keyauth_credentials: - - key: $SUPABASE_ANON_KEY - - username: service_role - keyauth_credentials: - - key: $SUPABASE_SERVICE_KEY - -acls: - - consumer: anon - group: anon - - consumer: service_role - group: admin - -basicauth_credentials: - - consumer: DASHBOARD - username: $DASHBOARD_USERNAME - password: $DASHBOARD_PASSWORD - -services: -`) - - builder.WriteString(fmt.Sprintf(` - name: auth-v1-open - url: http://%s-auth:9999/verify - routes: - - name: auth-v1-open - strip_path: true - paths: - - /auth/v1/verify - plugins: - - name: cors - -`, project.Name)) - builder.WriteString(fmt.Sprintf(` - name: auth-v1-open-callback - url: http://%s-auth:9999/callback - routes: - - name: auth-v1-open-callback - strip_path: true - paths: - - /auth/v1/callback - plugins: - - name: cors - -`, project.Name)) - builder.WriteString(fmt.Sprintf(` - name: auth-v1-open-authorize - url: http://%s-auth:9999/authorize - routes: - - name: auth-v1-open-authorize - strip_path: true - paths: - - /auth/v1/authorize - plugins: - - name: cors - -`, project.Name)) - builder.WriteString(fmt.Sprintf(` - name: auth-v1 - url: http://%s-auth:9999/ - routes: - - name: auth-v1-all - strip_path: true - paths: - - /auth/v1/ - plugins: - - name: cors - - name: key-auth - config: - hide_credentials: false - - name: acl - config: - hide_groups_header: true - allow: - - admin - - anon - -`, project.Name)) - builder.WriteString(fmt.Sprintf(` - name: rest-v1 - url: http://%s-postgrest:3000/ - routes: - - name: rest-v1-all - strip_path: true - paths: - - /rest/v1/ - plugins: - - name: cors - - name: key-auth - config: - hide_credentials: true - - name: acl - config: - hide_groups_header: true - allow: - - admin - - anon - -`, project.Name)) - builder.WriteString(fmt.Sprintf(` - name: graphql-v1 - url: http://%s-postgrest:3000/rpc/graphql - routes: - - name: graphql-v1-all - strip_path: true - paths: - - /graphql/v1 - plugins: - - name: cors - - name: key-auth - config: - hide_credentials: true - - name: request-transformer - config: - add: - headers: - - Content-Profile:graphql_public - - name: acl - config: - hide_groups_header: true - allow: - - admin - - anon - -`, project.Name)) - builder.WriteString(fmt.Sprintf(` - name: realtime-v1-ws - url: http://%s-realtime:4000/socket - protocol: ws - routes: - - name: realtime-v1-ws - strip_path: true - paths: - - /realtime/v1/ - plugins: - - name: cors - - name: key-auth - config: - hide_credentials: false - - name: acl - config: - hide_groups_header: true - allow: - - admin - - anon - -`, project.Name)) - builder.WriteString(fmt.Sprintf(` - name: realtime-v1-rest - url: http://%s-realtime:4000/api - routes: - - name: realtime-v1-rest - strip_path: true - paths: - - /realtime/v1/api - plugins: - - name: cors - - name: key-auth - config: - hide_credentials: false - - name: acl - config: - hide_groups_header: true - allow: - - admin - - anon - -`, project.Name)) - builder.WriteString(fmt.Sprintf(` - name: storage-v1 - url: http://%s-storage:5000/ - routes: - - name: storage-v1-all - strip_path: true - paths: - - /storage/v1/ - plugins: - - name: cors - -`, project.Name)) - builder.WriteString(fmt.Sprintf(` - name: meta - url: http://%s-meta:8080/ - routes: - - name: meta-all - strip_path: true - paths: - - /pg/ - plugins: - - name: key-auth - config: - hide_credentials: false - - name: acl - config: - hide_groups_header: true - allow: - - admin - -`, project.Name)) - builder.WriteString(fmt.Sprintf(` - name: dashboard - url: http://%s-studio:3000/ - routes: - - name: dashboard-all - strip_path: true - paths: - - / - plugins: - - name: cors - - name: basic-auth - config: - hide_credentials: true -`, project.Name)) - - kongConfig := builder.String() + kongConfig := strings.ReplaceAll(kongDeclarativeConfigTemplate, "{{PROJECT}}", project.Name) return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -439,7 +655,10 @@ services: Namespace: project.Namespace, Labels: labels, }, - Data: map[string]string{"kong.yml": kongConfig}, + Data: map[string]string{ + "kong.yml": kongConfig, + "kong-entrypoint.sh": kongEntrypointScript, + }, } } diff --git a/internal/component/postgrest.go b/internal/component/postgrest.go index b9cfb69..6ced036 100644 --- a/internal/component/postgrest.go +++ b/internal/component/postgrest.go @@ -116,7 +116,7 @@ func (b *PostgRESTBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (* Value: "anon", }, { - Name: "PGRST_DB_SCHEMA", + Name: "PGRST_DB_SCHEMAS", Value: "public", }, { diff --git a/internal/component/realtime.go b/internal/component/realtime.go index 6846504..7cfdd3e 100644 --- a/internal/component/realtime.go +++ b/internal/component/realtime.go @@ -110,7 +110,26 @@ func (b *RealtimeBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*a Value: fmt.Sprintf("postgresql://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=%s", sslMode), }, { - Name: "JWT_SECRET", + Name: "DB_AFTER_CONNECT_QUERY", + Value: "SET search_path TO _realtime", + }, + { + Name: "DB_ENC_KEY", + Value: "supabaserealtime", + }, + { + Name: "API_JWT_SECRET", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: project.Name + "-jwt", + }, + Key: "jwt-secret", + }, + }, + }, + { + Name: "METRICS_JWT_SECRET", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ @@ -144,7 +163,29 @@ func (b *RealtimeBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*a Value: "10000", }, { - Name: "SECURE_CHANNELS", + Name: "ERL_AFLAGS", + Value: "-proto_dist inet_tcp", + }, + { + Name: "DNS_NODES", + Value: "''", + }, + { + Name: "SEED_SELF_HOST", + Value: "true", + }, + // The tenant external id must match the first DNS label of the host + // Kong proxies to, i.e. the realtime Service name. + { + Name: "SELF_HOST_TENANT_NAME", + Value: project.Name + "-realtime", + }, + { + Name: "RUN_JANITOR", + Value: "true", + }, + { + Name: "DISABLE_HEALTHCHECK_LOGGING", Value: "true", }, } diff --git a/internal/component/storage.go b/internal/component/storage.go index 3285854..9885464 100644 --- a/internal/component/storage.go +++ b/internal/component/storage.go @@ -71,7 +71,7 @@ func (b *StorageBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*ap }, }, { - Name: "PGRST_JWT_SECRET", + Name: "AUTH_JWT_SECRET", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ diff --git a/internal/component/studio.go b/internal/component/studio.go index 200f454..0ecf109 100644 --- a/internal/component/studio.go +++ b/internal/component/studio.go @@ -62,6 +62,35 @@ func (b *StudioBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*app {Name: "STUDIO_PG_META_URL", Value: metaURL}, {Name: "NEXT_PUBLIC_ENABLE_LOGS", Value: "false"}, {Name: "NEXT_ANALYTICS_BACKEND_PROVIDER", Value: "postgres"}, + {Name: "ENABLED_FEATURES_LOGS_ALL", Value: "false"}, + {Name: "POSTGRES_USER_READ_WRITE", Value: "postgres"}, + { + Name: "POSTGRES_HOST", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: project.Spec.Database.SecretRef.Name}, + Key: "host", + }, + }, + }, + { + Name: "POSTGRES_PORT", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: project.Spec.Database.SecretRef.Name}, + Key: "port", + }, + }, + }, + { + Name: "POSTGRES_DB", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: project.Spec.Database.SecretRef.Name}, + Key: "database", + }, + }, + }, { Name: "POSTGRES_PASSWORD", ValueFrom: &corev1.EnvVarSource{ diff --git a/internal/webhook/supabaseproject_webhook.go b/internal/webhook/supabaseproject_webhook.go index 0b5b6d8..d615461 100644 --- a/internal/webhook/supabaseproject_webhook.go +++ b/internal/webhook/supabaseproject_webhook.go @@ -229,7 +229,7 @@ func (r *SupabaseProjectWebhook) validateImages(project *supabasev1alpha1.Supaba } if !strings.Contains(image, ":") { - return fmt.Errorf("image must include tag (e.g., 'kong:2.8.1')") + return fmt.Errorf("image must include tag (e.g., 'kong/kong:3.9.1')") } _ = component diff --git a/internal/webhook/supabaseproject_webhook_test.go b/internal/webhook/supabaseproject_webhook_test.go index 9bfb155..440dd34 100644 --- a/internal/webhook/supabaseproject_webhook_test.go +++ b/internal/webhook/supabaseproject_webhook_test.go @@ -337,7 +337,7 @@ func TestValidateCreate_ImageReferenceValidation(t *testing.T) { { name: "valid kong image", config: &supabasev1alpha1.KongConfig{ - Image: "kong:2.8.1", + Image: "kong/kong:3.9.1", }, wantErr: false, }, @@ -369,7 +369,7 @@ func TestValidateCreate_ImageReferenceValidation(t *testing.T) { Image: "kong", }, wantErr: true, - errMsg: "image must include tag (e.g., 'kong:2.8.1')", + errMsg: "image must include tag (e.g., 'kong/kong:3.9.1')", }, } diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 475a7fd..8325151 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -429,7 +429,7 @@ spec: time.Sleep(10 * time.Second) By("updating the SupabaseProject spec") - patchJSON := `{"spec":{"kong":{"image":"kong:2.8.2"}}}` + patchJSON := `{"spec":{"kong":{"image":"kong/kong:3.9.0"}}}` cmd := exec.Command("kubectl", "patch", "supabaseproject", projectName, "-n", testNamespace, "--type=merge", From 7dd5add248b54505b104514ca0694a27b82b1b62 Mon Sep 17 00:00:00 2001 From: Zhiqiang ZHOU Date: Fri, 24 Jul 2026 20:21:52 -0700 Subject: [PATCH 3/4] fix(e2e): wait for Kong rollout to settle before picking pod in T107 T106 patches the Kong image, and T107 re-applying the CR reverts it to the default, triggering a rolling update. T107 could pick a Ready pod from the old ReplicaSet that gets terminated mid-test, killing the port-forward. Wait for the deployment rollout to complete first. --- test/e2e/e2e_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 8325151..08eee5a 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -469,6 +469,19 @@ spec: } Eventually(verifyRunning, 3*time.Minute).Should(Succeed()) + // The previous spec (T106) patches the Kong image, which can leave a + // rollout in progress. Wait for it to settle so the pod picked below + // is not an old-generation pod about to be terminated. + By("waiting for Kong deployment rollout to complete") + Eventually(func() error { + cmd := exec.Command("kubectl", "rollout", "status", + fmt.Sprintf("deployment/%s-kong", projectName), + "-n", testNamespace, + "--timeout=10s") + _, err := utils.Run(cmd) + return err + }, 4*time.Minute, 5*time.Second).Should(Succeed()) + By("waiting for Kong pod to report Ready") var kongPodName string Eventually(func() error { From 0458d5defd08f7c5820dd2668f4db3a995acc7b5 Mon Sep 17 00:00:00 2001 From: Zhiqiang ZHOU Date: Fri, 24 Jul 2026 21:04:02 -0700 Subject: [PATCH 4/4] fix: reconcile Kong ConfigMap updates and source Studio db user from secret The Kong ConfigMap was only created when absent, so template changes (including the new kong-entrypoint.sh) never propagated to existing projects. Reconcile it with create-or-update and stop swallowing non-NotFound Get errors. Studio's POSTGRES_USER_READ_WRITE was hard-coded to postgres while its password comes from the database secret; use the secret's username so the credential pair stays consistent. --- internal/component/studio.go | 10 +++++++++- internal/controller/supabaseproject_controller.go | 11 ++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/internal/component/studio.go b/internal/component/studio.go index 0ecf109..46bcdfb 100644 --- a/internal/component/studio.go +++ b/internal/component/studio.go @@ -63,7 +63,15 @@ func (b *StudioBuilder) BuildDeployment(project *v1alpha1.SupabaseProject) (*app {Name: "NEXT_PUBLIC_ENABLE_LOGS", Value: "false"}, {Name: "NEXT_ANALYTICS_BACKEND_PROVIDER", Value: "postgres"}, {Name: "ENABLED_FEATURES_LOGS_ALL", Value: "false"}, - {Name: "POSTGRES_USER_READ_WRITE", Value: "postgres"}, + { + Name: "POSTGRES_USER_READ_WRITE", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: project.Spec.Database.SecretRef.Name}, + Key: "username", + }, + }, + }, { Name: "POSTGRES_HOST", ValueFrom: &corev1.EnvVarSource{ diff --git a/internal/controller/supabaseproject_controller.go b/internal/controller/supabaseproject_controller.go index a88deb4..028a76c 100644 --- a/internal/controller/supabaseproject_controller.go +++ b/internal/controller/supabaseproject_controller.go @@ -201,11 +201,20 @@ func (r *SupabaseProjectReconciler) reconcileAllComponents(ctx context.Context, return componentsStatus, err } existingKongConfigMap := &corev1.ConfigMap{} - if err := r.Get(ctx, client.ObjectKey{Namespace: kongConfigMap.Namespace, Name: kongConfigMap.Name}, existingKongConfigMap); err != nil && apierrors.IsNotFound(err) { + if err := r.Get(ctx, client.ObjectKey{Namespace: kongConfigMap.Namespace, Name: kongConfigMap.Name}, existingKongConfigMap); err != nil { + if !apierrors.IsNotFound(err) { + return componentsStatus, err + } if err := r.Create(ctx, kongConfigMap); err != nil { logger.Error(err, "Failed to create Kong ConfigMap") return componentsStatus, err } + } else { + existingKongConfigMap.Data = kongConfigMap.Data + if err := r.Update(ctx, existingKongConfigMap); err != nil { + logger.Error(err, "Failed to update Kong ConfigMap") + return componentsStatus, err + } } if err := componentReconciler.ReconcileComponent(ctx, project, &component.KongBuilder{}); err != nil {