chore: update migrations & images and switch to new registry

This commit is contained in:
Peter 2025-04-01 20:27:03 +02:00
parent 87a06dac66
commit b4347cc8a2
Signed by: prskr
GPG key ID: F56BED6903BC5E37
23 changed files with 388 additions and 37 deletions

View file

@ -13,7 +13,7 @@ on:
- "v*"
env:
MINOR_VERSIONS: '{"15": "12","17": "4"}'
MINOR_VERSIONS: '{"15":"12","17":"4"}'
jobs:
build:

View file

@ -22,9 +22,9 @@ jobs:
- name: Login to container registry
uses: docker/login-action@v3
with:
registry: code.icb4dc0.de
username: prskr
password: ${{ secrets.RELEASE_TOKEN }}
registry: registry.icb4dc0.de
username: ${{ secrets.HARBOR_USER }}
password: ${{ secrets.HARBOR_TOKEN }}
- name: Setup Go
uses: actions/setup-go@v5

View file

@ -8,7 +8,7 @@ before:
- go mod tidy
- go run mage.go GenerateAll
- mkdir -p out
- sh -c "cd config/release/default && kustomize edit set image supabase-operator=code.icb4dc0.de/prskr/supabase-operator:{{.Version}}"
- sh -c "cd config/release/default && kustomize edit set image supabase-operator=registry.icb4dc0.de/supabase-operator/controller:{{.Version}}"
- sh -c "kustomize build config/release/default > out/operator_manifest.yaml"
builds:
@ -28,7 +28,7 @@ kos:
base_image: gcr.io/distroless/static:nonroot
user: "65532:65532"
repositories:
- code.icb4dc0.de/prskr/supabase-operator
- registry.icb4dc0.de/supabase-operator/controller
platforms:
- linux/amd64
- linux/arm64

View file

@ -3,7 +3,6 @@
# git hook pre commit
pre-commit = [
"go mod tidy -go=1.24",
"go run mage.go GenerateAll",
"husky lint-staged",
# "golangci-lint run",
]

47
.zed/tasks.json Normal file
View file

@ -0,0 +1,47 @@
[
{
"label": "Tilt Up",
"command": "tilt",
"args": ["up"],
"use_new_terminal": false,
"allow_concurrent_runs": false
},
{
"label": "Generate CRDs",
"command": "go",
"args": ["run", "mage.go", "CRDs"],
"use_new_terminal": false,
"allow_concurrent_runs": false,
"tags": ["mage", "generate"]
},
{
"label": "Generate CRD docs",
"command": "go",
"args": ["run", "mage.go", "CRDDocs"],
"use_new_terminal": false,
"allow_concurrent_runs": false,
"tags": ["mage", "generate"]
},
{
"label": "Update image meta",
"command": "go",
"args": ["run", "mage.go", "FetchImageMeta"],
"use_new_terminal": false,
"allow_concurrent_runs": false,
"tags": ["mage"]
},
{
"label": "Update DB migrations",
"command": "go",
"args": ["run", "mage.go", "FetchMigrations"],
"use_new_terminal": false,
"allow_concurrent_runs": false,
"tags": ["mage"]
},
{
"label": "Run all Go tests",
"command": "go tool gotestsum ./...",
"use_new_terminal": false,
"allow_concurrent_runs": false
}
]

View file

@ -25,7 +25,7 @@ local_resource(
k8s_kind('Cluster', api_version='postgresql.cnpg.io/v1')
docker_build_with_restart(
'supabase-operator',
'controller',
'.',
entrypoint=['/app/bin/supabase-operator'],
dockerfile='dev/Dockerfile',

View file

@ -18,7 +18,15 @@ grant pg_read_all_data to supabase_read_only_user;
create schema if not exists extensions;
create extension if not exists "uuid-ossp" with schema extensions;
create extension if not exists pgcrypto with schema extensions;
create extension if not exists pgjwt with schema extensions;
do $$
begin
if exists (select 1 from pg_available_extensions where name = 'pgjwt') then
if not exists (select 1 from pg_extension where extname = 'pgjwt') then
create extension if not exists pgjwt with schema "extensions" cascade;
end if;
end if;
end $$;
-- Set up auth roles for the developer
create role anon nologin noinherit;

View file

@ -0,0 +1,26 @@
-- migrate:up
do $$
declare
ext_schema text;
extensions_schema_exists boolean;
begin
-- check if the "extensions" schema exists
select exists (
select 1 from pg_namespace where nspname = 'extensions'
) into extensions_schema_exists;
if extensions_schema_exists then
-- check if the "orioledb" extension is in the "public" schema
select nspname into ext_schema
from pg_extension e
join pg_namespace n on e.extnamespace = n.oid
where extname = 'orioledb';
if ext_schema = 'public' then
execute 'alter extension orioledb set schema extensions';
end if;
end if;
end $$;
-- migrate:down

View file

@ -0,0 +1,31 @@
-- migrate:up
DO $$
BEGIN
IF EXISTS (SELECT FROM pg_extension WHERE extname = 'pgsodium') THEN
CREATE OR REPLACE FUNCTION pgsodium.mask_role(masked_role regrole, source_name text, view_name text)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO ''
AS $function$
BEGIN
EXECUTE format(
'GRANT SELECT ON pgsodium.key TO %s',
masked_role);
EXECUTE format(
'GRANT pgsodium_keyiduser, pgsodium_keyholder TO %s',
masked_role);
EXECUTE format(
'GRANT ALL ON %I TO %s',
view_name,
masked_role);
RETURN;
END
$function$;
END IF;
END $$;
-- migrate:down

View file

@ -0,0 +1,64 @@
-- migrate:up
CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_event_trigger_ddl_commands() AS ev
JOIN pg_extension AS ext
ON ev.objid = ext.oid
WHERE ext.extname = 'pg_net'
)
THEN
IF NOT EXISTS (
SELECT 1
FROM pg_roles
WHERE rolname = 'supabase_functions_admin'
)
THEN
CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;
END IF;
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
IF EXISTS (
SELECT FROM pg_extension
WHERE extname = 'pg_net'
-- all versions in use on existing projects as of 2025-02-20
-- version 0.12.0 onwards don't need these applied
AND extversion IN ('0.2', '0.6', '0.7', '0.7.1', '0.8', '0.10.0', '0.11.0')
) THEN
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
END IF;
END IF;
END;
$$;
DO $$
BEGIN
IF EXISTS (SELECT FROM pg_extension WHERE extname = 'pg_net')
THEN
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY INVOKER;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY INVOKER;
REVOKE EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM supabase_functions_admin, postgres, anon, authenticated, service_role;
REVOKE EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM supabase_functions_admin, postgres, anon, authenticated, service_role;
GRANT ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO PUBLIC;
GRANT ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO PUBLIC;
END IF;
END $$;
-- migrate:down

View file

@ -25,7 +25,7 @@ spec:
containers:
- args:
- control-plane
image: supabase-operator:latest
image: controller:latest
name: control-plane
env:
- name: CONTROL_PLANE_NAMESPACE

View file

@ -44,7 +44,7 @@ metadata:
namespace: supabase-demo
spec:
instances: 1
imageName: code.icb4dc0.de/prskr/supabase-operator/postgres:17.2
imageName: registry.icb4dc0.de/supabase-operator/postgres:17.4
imagePullPolicy: Always
postgresUID: 26
postgresGID: 102

View file

@ -29,7 +29,7 @@ spec:
- manager
- --leader-elect
- --health-probe-bind-address=:8081
image: supabase-operator:latest
image: controller:latest
name: manager
env:
- name: CONTROLLER_NAMESPACE

View file

@ -2,9 +2,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: supabase-operator
newName: code.icb4dc0.de/prskr/supabase-operator
newTag: 0.1.0-SNAPSHOT-e6c7d68
- name: controller
newName: registry.icb4dc0.de/supabase-operator/controller
newTag: 0.1.0-SNAPSHOT-e6c7d68
resources:
- ../../default
- ../../default

View file

@ -1,7 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export DATABASE_URL="postgres://supabase_admin:1n1t-R00t!@localhost:5432/app"
go run mage.go Migrate

View file

@ -0,0 +1,16 @@
package controller_test
import (
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
)
var testingScheme = runtime.NewScheme()
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(testingScheme))
utilruntime.Must(supabasev1alpha1.AddToScheme(testingScheme))
}

View file

@ -0,0 +1,81 @@
package controller_test
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
"code.icb4dc0.de/prskr/supabase-operator/internal/controller"
)
func TestStorageS3CredsController(t *testing.T) {
storage := new(supabasev1alpha1.Storage)
MustReadObject(t, filepath.Join("testdata", "storage_basic.yaml"), storage)
t.Log(string(MustMarshal(t, storage)))
clientSet := fake.NewClientBuilder().
WithObjects(storage).
WithScheme(testingScheme).
Build()
log.SetLogger(zap.New(zap.UseDevMode(true)))
controllerReconciler := &controller.StorageS3CredentialsReconciler{
Client: clientSet,
Scheme: testingScheme,
}
_, err := controllerReconciler.Reconcile(t.Context(), reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(storage),
})
assert.NoError(t, err)
}
func MustMarshal(t testing.TB, obj runtime.Object) []byte {
t.Helper()
raw, err := json.Marshal(obj)
if err != nil {
t.Errorf("failed to marshal object: %v", err)
t.Fail()
}
return raw
}
func MustReadObject(t testing.TB, relativePath string, obj runtime.Object) {
t.Helper()
gkvs, _, err := testingScheme.ObjectKinds(obj)
if err != nil {
t.Errorf("failed to determine group version kind: %v", err)
t.Fatal(err)
}
decoder := scheme.Codecs.UniversalDecoder(testingScheme.PreferredVersionAllGroups()...)
raw, err := os.ReadFile(relativePath)
if err != nil {
t.Errorf("failed to read file %s: %v", relativePath, err)
t.Fail()
}
_, _, err = decoder.Decode(raw, &gkvs[0], obj)
if err != nil {
t.Errorf("failed to unmarshal file %s: %v", relativePath, err)
t.Fail()
}
}

View file

@ -0,0 +1,28 @@
---
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
kind: Storage
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: storage-sample
namespace: supabase-demo
spec:
api:
s3:
credentialsSecretRef:
secretName: "storage-api-s3-credentials"
accessKeyIdKey: accessKeyId
accessSecretKeyKey: secretAccessKey
db:
host: cluster-example-rw.supabase-demo.svc
dbName: app
dbCredentialsRef:
# will be created by Core resource operator if not present
# just make sure the secret name is either based on the name of the core resource or explicitly set
# format <core-resource-name>-db-creds-supabase-storage-admin
secretName: core-sample-db-creds-supabase-storage-admin
jwtAuth:
# will be created by Core resource operator if not present
# just make sure the secret name is either based on the name of the core resource or explicitly set
secretName: core-sample-jwt

View file

@ -55,7 +55,8 @@ func (m Migrator) ApplyAll(
logger.Info("Applying missing or outdated migration", "filename", s.FileName)
err := status.Database.RecordMigrationCondition(s.FileName, s.Hash, m.Apply(ctx, s.Content))
if err != nil {
return false, err
logger.Error(err, "Failed to apply migrations", "filename", s.FileName)
continue
}
appliedSomething = true

View file

@ -37,7 +37,7 @@ type studioDefaults struct {
func studioServiceConfig() serviceConfig[studioEnvKeys, studioDefaults] {
return serviceConfig[studioEnvKeys, studioDefaults]{
Name: "studio",
LivenessProbePath: "/api/profile",
LivenessProbePath: "/api/platform/profile",
EnvKeys: studioEnvKeys{
PGMetaURL: "STUDIO_PG_META_URL",
DBPassword: "POSTGRES_PASSWORD",
@ -47,7 +47,7 @@ func studioServiceConfig() serviceConfig[studioEnvKeys, studioDefaults] {
AnonKey: "SUPABASE_ANON_KEY",
ServiceKey: "SUPABASE_SERVICE_KEY",
Host: fixedEnvOf("HOSTNAME", "0.0.0.0"),
LogsEnabled: fixedEnvOf("NEXT_PUBLIC_ENABLE_LOGS", "true"),
LogsEnabled: fixedEnvOf("NEXT_PUBLIC_ENABLE_LOGS", "false"),
},
Defaults: studioDefaults{
NodeUID: 1000,

View file

@ -44,6 +44,7 @@ const (
var ignoredMigrations = []string{
"10000000000000_demote-postgres.sql",
"20250312095419_pgbouncer_ownership.sql",
}
func GenerateAll(ctx context.Context) {
@ -147,7 +148,7 @@ func FetchImageMeta(ctx context.Context) (err error) {
}
}
latestEnvoyTag, err := latestReleaseVersion(ctx, "envoyproxy", "envoy")
latestEnvoyTag, err := latestReleaseVersion(ctx, "envoyproxy", "envoy", excludeDrafts, excludePreReleases)
if err != nil {
return err
}
@ -169,11 +170,13 @@ func FetchImageMeta(ctx context.Context) (err error) {
}
func FetchMigrations(ctx context.Context) (err error) {
latestRelease, err := latestReleaseVersion(ctx, "supabase", "postgres")
latestRelease, err := latestReleaseVersion(ctx, "supabase", "postgres", excludeDrafts, excludePreReleases, matchesTagPattern(`15\..*`))
if err != nil {
return err
}
slog.InfoContext(ctx, "Extracting Postgres migrations for release", slog.String("release", latestRelease))
releaseArtifactURL := fmt.Sprintf("https://github.com/supabase/postgres/archive/refs/tags/%s.tar.gz", latestRelease)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, releaseArtifactURL, nil)
if err != nil {

View file

@ -21,12 +21,42 @@ import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"code.icb4dc0.de/prskr/supabase-operator/internal/errx"
)
func latestReleaseVersion(ctx context.Context, owner, repo string) (tagName string, err error) {
releaseURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
type releaseMatcher interface {
matchesRelease(r release) bool
}
var (
excludeDrafts releaseMatcher = releaseMatcherFunc(func(r release) bool {
return !r.Draft
})
excludePreReleases releaseMatcher = releaseMatcherFunc(func(r release) bool {
return !r.PreRelease
})
)
func matchesTagPattern(pattern string) releaseMatcher {
compiled, err := regexp.Compile(pattern)
if err != nil {
panic(err)
}
return releaseMatcherFunc(func(r release) bool {
return compiled.MatchString(r.TagName)
})
}
type release struct {
TagName string `json:"tag_name"`
Draft bool `json:"draft"`
PreRelease bool `json:"prerelease"`
}
func latestReleaseVersion(ctx context.Context, owner, repo string, matchers ...releaseMatcher) (tagName string, err error) {
releaseURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repo)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, releaseURL, nil)
if err != nil {
return "", err
@ -43,13 +73,37 @@ func latestReleaseVersion(ctx context.Context, owner, repo string) (tagName stri
return "", fmt.Errorf("failed to retrieve latest release: %s", resp.Status)
}
var release struct {
TagName string `json:"tag_name"`
}
var (
releases []release
matcher = multiMatcher(matchers)
)
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
return "", err
}
return release.TagName, nil
for _, release := range releases {
if matcher.matchesRelease(release) {
return release.TagName, nil
}
}
return "", fmt.Errorf("no release found matching the criteria: %s/%s", owner, repo)
}
type multiMatcher []releaseMatcher
func (m multiMatcher) matchesRelease(r release) bool {
for _, matcher := range m {
if !matcher.matchesRelease(r) {
return false
}
}
return true
}
type releaseMatcherFunc func(r release) bool
func (f releaseMatcherFunc) matchesRelease(r release) bool {
return f(r)
}

View file

@ -21,7 +21,7 @@ public class TaskList : BaseModel
[PrimaryKey("id")]
public int Id { get; set; }
[Column("user_id")]
public int UserId { get; set; }
public Guid UserId { get; set; }
[Column("name")]
public string Name { get; set; }
}
}