From 2d71a4a132e75623fdffc0745e9205baa04667ff Mon Sep 17 00:00:00 2001 From: Peter Kurfer <peter@icb4dc0.de> Date: Thu, 13 Feb 2025 09:29:47 +0100 Subject: [PATCH] fix(db): track state of migrations and execute them again when necessary --- .github/workflows/postgres.yml | 4 +++- Dockerfile | 5 +---- Makefile | 2 +- Tiltfile | 7 ------- api/v1alpha1/core_types.go | 18 ++++++++++++++++-- assets/migrations/setup/realtime.sql | 3 +++ config/dev/cnpg-cluster.yaml | 2 +- config/dev/kustomizeconfig/cnpg-cluster.yaml | 4 ++++ dev/cluster.yaml | 5 +++-- docs/api/supabase.k8s.icb4dc0.de.md | 16 +++++++++++++--- internal/controller/core_db_controller.go | 8 ++------ internal/db/migrator.go | 11 ++++++++--- .../v1alpha1/apigateway_webhook_validator.go | 1 + test/e2e/e2e_suite_test.go | 2 +- test/e2e/e2e_test.go | 4 ++-- 15 files changed, 59 insertions(+), 33 deletions(-) create mode 100644 assets/migrations/setup/realtime.sql create mode 100644 config/dev/kustomizeconfig/cnpg-cluster.yaml diff --git a/.github/workflows/postgres.yml b/.github/workflows/postgres.yml index 4ceb7a7..258f692 100644 --- a/.github/workflows/postgres.yml +++ b/.github/workflows/postgres.yml @@ -63,6 +63,8 @@ jobs: - name: Create manifest run: | - docker buildx imagetools create -t code.icb4dc0.de/prskr/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}.${{ github.run_number }} \ + docker buildx imagetools create \ + -t code.icb4dc0.de/prskr/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }} \ + -t code.icb4dc0.de/prskr/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}.${{ github.run_number }} \ code.icb4dc0.de/prskr/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}.${{ github.run_number }}-arm64 \ code.icb4dc0.de/prskr/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}.${{ github.run_number }}-amd64 diff --git a/Dockerfile b/Dockerfile index 8c65e73..2338b64 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.23.4 AS builder +FROM golang:1.23.6-alpine AS builder ARG TARGETOS ARG TARGETARCH @@ -16,10 +16,7 @@ COPY [ "go.*", "./" ] COPY [ "api", "api" ] COPY [ "assets/migrations", "assets/migrations" ] COPY [ "cmd", "cmd" ] -COPY [ "infrastructure", "infrastructure" ] COPY [ "internal", "internal" ] -COPY [ "magefiles", "magefiles" ] -COPY [ "tools", "tools" ] # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command diff --git a/Makefile b/Makefile index 0c4be10..faf388e 100644 --- a/Makefile +++ b/Makefile @@ -148,7 +148,7 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + cd config/manager && $(KUSTOMIZE) edit set image supabase-operator=${IMG} $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - .PHONY: undeploy diff --git a/Tiltfile b/Tiltfile index adaed2b..6f29284 100644 --- a/Tiltfile +++ b/Tiltfile @@ -7,13 +7,6 @@ k8s_yaml(kustomize('config/dev')) compile_cmd = 'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out/supabase-operator ./cmd/' -update_settings(suppress_unused_image_warnings=["localhost:5005/cnpg-postgres:17.2"]) -custom_build( - 'localhost:5005/cnpg-postgres:17.2', - 'docker build -t $EXPECTED_REF --push -f postgres/Dockerfile --build-arg POSTGRES_MAJOR=17 --build-arg=POSTGRES_MINOR=2 .', - ['postgres/Dockerfile'] -) - local_resource( 'manager-go-compile', compile_cmd, diff --git a/api/v1alpha1/core_types.go b/api/v1alpha1/core_types.go index bbd6148..744035c 100644 --- a/api/v1alpha1/core_types.go +++ b/api/v1alpha1/core_types.go @@ -429,12 +429,12 @@ func (s DatabaseStatus) IsMigrationUpToDate(name string, hash []byte) (found boo return false, false } -func (s DatabaseStatus) RecordMigrationCondition(name string, hash []byte, err error) { +func (s *DatabaseStatus) RecordMigrationCondition(name string, hash []byte, err error) error { var ( now = time.Now() newStatus = MigrationConditionStatusApplied lastProbeTime = metav1.NewTime(now) - lastTransitionTime metav1.Time + lastTransitionTime = metav1.NewTime(now) message string ) @@ -456,8 +456,22 @@ func (s DatabaseStatus) RecordMigrationCondition(name string, hash []byte, err e cond.LastTransitionTime = lastTransitionTime cond.Reason = "Outdated" cond.Message = message + + s.MigrationConditions[idx] = cond + return err } } + + s.MigrationConditions = append(s.MigrationConditions, MigrationScriptCondition{ + Name: name, + Hash: hash, + Status: newStatus, + LastProbeTime: lastProbeTime, + LastTransitionTime: lastTransitionTime, + Message: message, + }) + + return err } type CoreConditionType string diff --git a/assets/migrations/setup/realtime.sql b/assets/migrations/setup/realtime.sql new file mode 100644 index 0000000..2d354df --- /dev/null +++ b/assets/migrations/setup/realtime.sql @@ -0,0 +1,3 @@ +create schema if not exists _realtime; + +alter schema _realtime owner to supabase_admin; diff --git a/config/dev/cnpg-cluster.yaml b/config/dev/cnpg-cluster.yaml index 089167f..c2a5b57 100644 --- a/config/dev/cnpg-cluster.yaml +++ b/config/dev/cnpg-cluster.yaml @@ -44,7 +44,7 @@ metadata: namespace: supabase-demo spec: instances: 1 - imageName: localhost:5005/cnpg-postgres:17.2 + imageName: code.icb4dc0.de/prskr/supabase-operator/postgres:17.2.258 imagePullPolicy: Always postgresUID: 26 postgresGID: 102 diff --git a/config/dev/kustomizeconfig/cnpg-cluster.yaml b/config/dev/kustomizeconfig/cnpg-cluster.yaml new file mode 100644 index 0000000..4dc7de1 --- /dev/null +++ b/config/dev/kustomizeconfig/cnpg-cluster.yaml @@ -0,0 +1,4 @@ +images: + - kind: Cluster + group: postgresql.cnpg.io + path: spec/imageName diff --git a/dev/cluster.yaml b/dev/cluster.yaml index 30355bb..265701c 100644 --- a/dev/cluster.yaml +++ b/dev/cluster.yaml @@ -1,12 +1,13 @@ apiVersion: ctlptl.dev/v1alpha1 kind: Registry -name: ctlptl-registry +name: supabase-operator-registry port: 5005 --- apiVersion: ctlptl.dev/v1alpha1 kind: Cluster product: kind -registry: ctlptl-registry +registry: supabase-operator-registry kindV1Alpha4Cluster: + name: supabase-operator-debug networking: ipFamily: dual diff --git a/docs/api/supabase.k8s.icb4dc0.de.md b/docs/api/supabase.k8s.icb4dc0.de.md index a017fa6..33b92cf 100644 --- a/docs/api/supabase.k8s.icb4dc0.de.md +++ b/docs/api/supabase.k8s.icb4dc0.de.md @@ -518,7 +518,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `appliedMigrations` _[MigrationStatus](#migrationstatus)_ | | | | +| `migrationConditions` _[MigrationScriptCondition](#migrationscriptcondition) array_ | | | | | `roles` _object (keys:string, values:integer array)_ | | | | @@ -773,9 +773,11 @@ _Appears in:_ | `serviceKey` _string_ | ServiceKey - key in secret where to read the service JWT from | service_key | | -#### MigrationStatus -_Underlying type:_ _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_ + +#### MigrationScriptCondition + + @@ -784,6 +786,14 @@ _Underlying type:_ _[Time](https://kubernetes.io/docs/reference/generated/kubern _Appears in:_ - [DatabaseStatus](#databasestatus) +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | Name - file name of the migration script | | | +| `hash` _integer array_ | Hash - SHA256 hash of the script when it was last successfully applied | | | +| `lastProbeTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_ | LastProbeTime - last time the operator tried to execute the migration script | | | +| `lastTransitionTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_ | LastTransitionTime - last time the condition transitioned from one status to another | | | +| `reason` _string_ | Reason - one-word, CamcelCase reason for the condition's last transition | | | +| `message` _string_ | Message - human-readable message indicating details about the last transition | | | #### OAuthProvider diff --git a/internal/controller/core_db_controller.go b/internal/controller/core_db_controller.go index 14dfb40..bac200f 100644 --- a/internal/controller/core_db_controller.go +++ b/internal/controller/core_db_controller.go @@ -105,11 +105,7 @@ func (r *CoreDbReconciler) applyMissingMigrations( var appliedSomething bool - if core.Status.Database.AppliedMigrations == nil { - core.Status.Database.AppliedMigrations = make(supabasev1alpha1.MigrationStatus) - } - - if appliedSomething, err = migrator.ApplyAll(ctx, core.Status.Database.AppliedMigrations, migrations.InitScripts()); err != nil { + if appliedSomething, err = migrator.ApplyAll(ctx, &core.Status, migrations.InitScripts(), true); err != nil { return err } @@ -120,7 +116,7 @@ func (r *CoreDbReconciler) applyMissingMigrations( logger.Info("Init scripts were up to date - did not run any") } - if appliedSomething, err = migrator.ApplyAll(ctx, core.Status.Database.AppliedMigrations, migrations.MigrationScripts()); err != nil { + if appliedSomething, err = migrator.ApplyAll(ctx, &core.Status, migrations.MigrationScripts(), false); err != nil { return err } diff --git a/internal/db/migrator.go b/internal/db/migrator.go index c1acce2..62d8130 100644 --- a/internal/db/migrator.go +++ b/internal/db/migrator.go @@ -32,7 +32,12 @@ type Migrator struct { Conn *pgx.Conn } -func (m Migrator) ApplyAll(ctx context.Context, status *supabasev1alpha1.CoreStatus, seq iter.Seq2[migrations.Script, error], areInitScripts bool) (appliedSomething bool, err error) { +func (m Migrator) ApplyAll( + ctx context.Context, + status *supabasev1alpha1.CoreStatus, + seq iter.Seq2[migrations.Script, error], + areInitScripts bool, +) (appliedSomething bool, err error) { logger := log.FromContext(ctx) for s, err := range seq { @@ -48,12 +53,12 @@ func (m Migrator) ApplyAll(ctx context.Context, status *supabasev1alpha1.CoreSta } logger.Info("Applying missing or outdated migration", "filename", s.FileName) - if err := m.Apply(ctx, s.Content); err != nil { + err := status.Database.RecordMigrationCondition(s.FileName, s.Hash, m.Apply(ctx, s.Content)) + if err != nil { return false, err } appliedSomething = true - status.Record(s.FileName) } return appliedSomething, nil diff --git a/internal/webhook/v1alpha1/apigateway_webhook_validator.go b/internal/webhook/v1alpha1/apigateway_webhook_validator.go index 6c7a1a3..d8cbab9 100644 --- a/internal/webhook/v1alpha1/apigateway_webhook_validator.go +++ b/internal/webhook/v1alpha1/apigateway_webhook_validator.go @@ -106,6 +106,7 @@ func (v *APIGatewayCustomValidator) ValidateDelete(ctx context.Context, obj runt return nil, nil } +//nolint:unparam // keep the warnings for future use cases func validateEnvoyControlPlane(gateway *supabasev1alpha1.APIGateway) (admission.Warnings, error) { envoySpec := gateway.Spec.Envoy diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index b5e4381..f98c79a 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -43,7 +43,7 @@ var ( // projectImage is the name of the image which will be build and loaded // with the code source changes to be tested. - projectImage = "example.com/supabase-operator:v0.0.1" + projectImage = "code.icb4dc0.de/prskr/supabase-operator:v0.0.1" ) // TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated, diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index e54e1e1..9c84ccf 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -31,13 +31,13 @@ import ( ) // namespace where the project is deployed in -const namespace = "supabase-operator-system" +const namespace = "supabase-system" // serviceAccountName created for the project const serviceAccountName = "supabase-operator-controller-manager" // metricsServiceName is the name of the metrics service of the project -const metricsServiceName = "supabase-operator-controller-manager-metrics-service" +const metricsServiceName = "supabase-controller-manager-metrics-service" // metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data const metricsRoleBindingName = "supabase-operator-metrics-binding"