fix(db): track state of migrations and execute them again when necessary

This commit is contained in:
Peter 2025-02-13 09:29:47 +01:00
parent 264b30e8a2
commit 2d71a4a132
Signed by: prskr
GPG key ID: F56BED6903BC5E37
15 changed files with 59 additions and 33 deletions

View file

@ -63,6 +63,8 @@ jobs:
- name: Create manifest - name: Create manifest
run: | 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 }}-arm64 \
code.icb4dc0.de/prskr/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}.${{ github.run_number }}-amd64 code.icb4dc0.de/prskr/supabase-operator/postgres:${{ matrix.postgres_major }}.${{ fromJSON(env.MINOR_VERSIONS)[matrix.postgres_major] }}.${{ github.run_number }}-amd64

View file

@ -1,5 +1,5 @@
# Build the manager binary # Build the manager binary
FROM golang:1.23.4 AS builder FROM golang:1.23.6-alpine AS builder
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
@ -16,10 +16,7 @@ COPY [ "go.*", "./" ]
COPY [ "api", "api" ] COPY [ "api", "api" ]
COPY [ "assets/migrations", "assets/migrations" ] COPY [ "assets/migrations", "assets/migrations" ]
COPY [ "cmd", "cmd" ] COPY [ "cmd", "cmd" ]
COPY [ "infrastructure", "infrastructure" ]
COPY [ "internal", "internal" ] COPY [ "internal", "internal" ]
COPY [ "magefiles", "magefiles" ]
COPY [ "tools", "tools" ]
# Build # Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command # the GOARCH has not a default value to allow the binary be built according to the host where the command

View file

@ -148,7 +148,7 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified
.PHONY: deploy .PHONY: deploy
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 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 - $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f -
.PHONY: undeploy .PHONY: undeploy

View file

@ -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/' 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( local_resource(
'manager-go-compile', 'manager-go-compile',
compile_cmd, compile_cmd,

View file

@ -429,12 +429,12 @@ func (s DatabaseStatus) IsMigrationUpToDate(name string, hash []byte) (found boo
return false, false 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 ( var (
now = time.Now() now = time.Now()
newStatus = MigrationConditionStatusApplied newStatus = MigrationConditionStatusApplied
lastProbeTime = metav1.NewTime(now) lastProbeTime = metav1.NewTime(now)
lastTransitionTime metav1.Time lastTransitionTime = metav1.NewTime(now)
message string message string
) )
@ -456,8 +456,22 @@ func (s DatabaseStatus) RecordMigrationCondition(name string, hash []byte, err e
cond.LastTransitionTime = lastTransitionTime cond.LastTransitionTime = lastTransitionTime
cond.Reason = "Outdated" cond.Reason = "Outdated"
cond.Message = message 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 type CoreConditionType string

View file

@ -0,0 +1,3 @@
create schema if not exists _realtime;
alter schema _realtime owner to supabase_admin;

View file

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

View file

@ -0,0 +1,4 @@
images:
- kind: Cluster
group: postgresql.cnpg.io
path: spec/imageName

View file

@ -1,12 +1,13 @@
apiVersion: ctlptl.dev/v1alpha1 apiVersion: ctlptl.dev/v1alpha1
kind: Registry kind: Registry
name: ctlptl-registry name: supabase-operator-registry
port: 5005 port: 5005
--- ---
apiVersion: ctlptl.dev/v1alpha1 apiVersion: ctlptl.dev/v1alpha1
kind: Cluster kind: Cluster
product: kind product: kind
registry: ctlptl-registry registry: supabase-operator-registry
kindV1Alpha4Cluster: kindV1Alpha4Cluster:
name: supabase-operator-debug
networking: networking:
ipFamily: dual ipFamily: dual

View file

@ -518,7 +518,7 @@ _Appears in:_
| Field | Description | Default | Validation | | Field | Description | Default | Validation |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `appliedMigrations` _[MigrationStatus](#migrationstatus)_ | | | | | `migrationConditions` _[MigrationScriptCondition](#migrationscriptcondition) array_ | | | |
| `roles` _object (keys:string, values:integer 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 | | | `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:_ _Appears in:_
- [DatabaseStatus](#databasestatus) - [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 #### OAuthProvider

View file

@ -105,11 +105,7 @@ func (r *CoreDbReconciler) applyMissingMigrations(
var appliedSomething bool var appliedSomething bool
if core.Status.Database.AppliedMigrations == nil { if appliedSomething, err = migrator.ApplyAll(ctx, &core.Status, migrations.InitScripts(), true); err != nil {
core.Status.Database.AppliedMigrations = make(supabasev1alpha1.MigrationStatus)
}
if appliedSomething, err = migrator.ApplyAll(ctx, core.Status.Database.AppliedMigrations, migrations.InitScripts()); err != nil {
return err return err
} }
@ -120,7 +116,7 @@ func (r *CoreDbReconciler) applyMissingMigrations(
logger.Info("Init scripts were up to date - did not run any") 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 return err
} }

View file

@ -32,7 +32,12 @@ type Migrator struct {
Conn *pgx.Conn 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) logger := log.FromContext(ctx)
for s, err := range seq { 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) 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 return false, err
} }
appliedSomething = true appliedSomething = true
status.Record(s.FileName)
} }
return appliedSomething, nil return appliedSomething, nil

View file

@ -106,6 +106,7 @@ func (v *APIGatewayCustomValidator) ValidateDelete(ctx context.Context, obj runt
return nil, nil return nil, nil
} }
//nolint:unparam // keep the warnings for future use cases
func validateEnvoyControlPlane(gateway *supabasev1alpha1.APIGateway) (admission.Warnings, error) { func validateEnvoyControlPlane(gateway *supabasev1alpha1.APIGateway) (admission.Warnings, error) {
envoySpec := gateway.Spec.Envoy envoySpec := gateway.Spec.Envoy

View file

@ -43,7 +43,7 @@ var (
// projectImage is the name of the image which will be build and loaded // projectImage is the name of the image which will be build and loaded
// with the code source changes to be tested. // 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, // TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated,

View file

@ -31,13 +31,13 @@ import (
) )
// namespace where the project is deployed in // namespace where the project is deployed in
const namespace = "supabase-operator-system" const namespace = "supabase-system"
// serviceAccountName created for the project // serviceAccountName created for the project
const serviceAccountName = "supabase-operator-controller-manager" const serviceAccountName = "supabase-operator-controller-manager"
// metricsServiceName is the name of the metrics service of the project // 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 // metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data
const metricsRoleBindingName = "supabase-operator-metrics-binding" const metricsRoleBindingName = "supabase-operator-metrics-binding"