refactor(db): extract Supabase migrations from release artifact
Some checks failed
Lint / Run on Ubuntu (push) Failing after 2m42s
E2E Tests / Run on Ubuntu (push) Failing after 3m44s
Tests / Run on Ubuntu (push) Failing after 3m53s

This commit is contained in:
Peter 2025-01-05 11:42:15 +01:00
parent 2ef37683cb
commit 7d9e518f86
Signed by: prskr
GPG key ID: F56BED6903BC5E37
20 changed files with 113 additions and 185 deletions

View file

@ -64,3 +64,10 @@ linters-settings:
alias: $1$2 alias: $1$2
- pkg: "k8s.io/apimachinery/pkg/apis/meta/v1" - pkg: "k8s.io/apimachinery/pkg/apis/meta/v1"
alias: metav1 alias: metav1
severity:
default-severity: error
rules:
- linters:
- godox
severity: info

View file

@ -3,9 +3,7 @@
# git hook pre commit # git hook pre commit
pre-commit = [ pre-commit = [
"go mod tidy -go=1.23", "go mod tidy -go=1.23",
"go run mage.go FetchImageMeta", "go run mage.go GenerateAll",
"go run mage.go CRDs",
"go run mage.go CRDDocs",
"husky lint-staged", "husky lint-staged",
# "golangci-lint run", # "golangci-lint run",
] ]

View file

@ -25,7 +25,6 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"time"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -381,7 +380,7 @@ type CoreSpec struct {
Auth *AuthSpec `json:"auth,omitempty"` Auth *AuthSpec `json:"auth,omitempty"`
} }
type MigrationStatus map[string]int64 type MigrationStatus map[string]metav1.Time
func (s MigrationStatus) IsApplied(name string) bool { func (s MigrationStatus) IsApplied(name string) bool {
_, ok := s[name] _, ok := s[name]
@ -389,7 +388,7 @@ func (s MigrationStatus) IsApplied(name string) bool {
} }
func (s MigrationStatus) Record(name string) { func (s MigrationStatus) Record(name string) {
s[name] = time.Now().UTC().UnixMilli() s[name] = metav1.Now()
} }
type DatabaseStatus struct { type DatabaseStatus struct {
@ -399,19 +398,9 @@ type DatabaseStatus struct {
type CoreConditionType string type CoreConditionType string
type CoreCondition struct {
Type CoreConditionType `json:"type"`
Status corev1.ConditionStatus `json:"status"`
LastProbeTime metav1.Time `json:"lastProbeTime,omitempty"`
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
Reason string `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
}
// CoreStatus defines the observed state of Core. // CoreStatus defines the observed state of Core.
type CoreStatus struct { type CoreStatus struct {
Database DatabaseStatus `json:"database,omitempty"` Database DatabaseStatus `json:"database,omitempty"`
Conditions []CoreCondition `json:"conditions,omitempty"`
} }
// +kubebuilder:object:root=true // +kubebuilder:object:root=true

View file

@ -21,7 +21,7 @@ limitations under the License.
package v1alpha1 package v1alpha1
import ( import (
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )
@ -319,23 +319,6 @@ func (in *Core) DeepCopyObject() runtime.Object {
return nil return nil
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CoreCondition) DeepCopyInto(out *CoreCondition) {
*out = *in
in.LastProbeTime.DeepCopyInto(&out.LastProbeTime)
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoreCondition.
func (in *CoreCondition) DeepCopy() *CoreCondition {
if in == nil {
return nil
}
out := new(CoreCondition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CoreList) DeepCopyInto(out *CoreList) { func (in *CoreList) DeepCopyInto(out *CoreList) {
*out = *in *out = *in
@ -399,13 +382,6 @@ func (in *CoreSpec) DeepCopy() *CoreSpec {
func (in *CoreStatus) DeepCopyInto(out *CoreStatus) { func (in *CoreStatus) DeepCopyInto(out *CoreStatus) {
*out = *in *out = *in
in.Database.DeepCopyInto(&out.Database) in.Database.DeepCopyInto(&out.Database)
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]CoreCondition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoreStatus. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoreStatus.
@ -631,7 +607,7 @@ func (in *DatabaseStatus) DeepCopyInto(out *DatabaseStatus) {
in, out := &in.AppliedMigrations, &out.AppliedMigrations in, out := &in.AppliedMigrations, &out.AppliedMigrations
*out = make(MigrationStatus, len(*in)) *out = make(MigrationStatus, len(*in))
for key, val := range *in { for key, val := range *in {
(*out)[key] = val (*out)[key] = *val.DeepCopy()
} }
} }
if in.Roles != nil { if in.Roles != nil {
@ -806,7 +782,7 @@ func (in MigrationStatus) DeepCopyInto(out *MigrationStatus) {
in := &in in := &in
*out = make(MigrationStatus, len(*in)) *out = make(MigrationStatus, len(*in))
for key, val := range *in { for key, val := range *in {
(*out)[key] = val (*out)[key] = *val.DeepCopy()
} }
} }
} }

View file

@ -73,7 +73,8 @@ func (p controlPlane) Run(ctx context.Context, cache cache.SnapshotCache) (err e
// gRPC golang library sets a very small upper bound for the number gRPC/h2 // gRPC golang library sets a very small upper bound for the number gRPC/h2
// streams over a single TCP connection. If a proxy multiplexes requests over // streams over a single TCP connection. If a proxy multiplexes requests over
// a single connection to the management server, then it might lead to // a single connection to the management server, then it might lead to
// availability problems. Keepalive timeouts based on connection_keepalive parameter https://www.envoyproxy.io/docs/envoy/latest/configuration/overview/examples#dynamic // availability problems. Keepalive timeouts based on connection_keepalive parameter
// https://www.envoyproxy.io/docs/envoy/latest/configuration/overview/examples#dynamic
grpcOptions := append(make([]grpc.ServerOption, 0, 4), grpcOptions := append(make([]grpc.ServerOption, 0, 4),
grpc.MaxConcurrentStreams(grpcMaxConcurrentStreams), grpc.MaxConcurrentStreams(grpcMaxConcurrentStreams),
@ -139,6 +140,7 @@ func (p controlPlane) Run(ctx context.Context, cache cache.SnapshotCache) (err e
return err return err
} }
//nolint:unparam // signature required by kong
func (p controlPlane) AfterApply(kongctx *kong.Context) error { func (p controlPlane) AfterApply(kongctx *kong.Context) error {
kongctx.BindTo(cache.NewSnapshotCache(false, cache.IDHash{}, nil), (*cache.SnapshotCache)(nil)) kongctx.BindTo(cache.NewSnapshotCache(false, cache.IDHash{}, nil), (*cache.SnapshotCache)(nil))
return nil return nil

View file

@ -59,6 +59,7 @@ type app struct {
} `embed:"" prefix:"logging."` } `embed:"" prefix:"logging."`
} }
//nolint:unparam // signature required by kong
func (a app) AfterApply(kongctx *kong.Context) error { func (a app) AfterApply(kongctx *kong.Context) error {
opts := zap.Options{ opts := zap.Options{
Development: a.Logging.Development, Development: a.Logging.Development,

View file

@ -1779,34 +1779,12 @@ spec:
status: status:
description: CoreStatus defines the observed state of Core. description: CoreStatus defines the observed state of Core.
properties: properties:
conditions:
items:
properties:
lastProbeTime:
format: date-time
type: string
lastTransitionTime:
format: date-time
type: string
message:
type: string
reason:
type: string
status:
type: string
type:
type: string
required:
- status
- type
type: object
type: array
database: database:
properties: properties:
appliedMigrations: appliedMigrations:
additionalProperties: additionalProperties:
format: int64 format: date-time
type: integer type: string
type: object type: object
roles: roles:
additionalProperties: additionalProperties:

View file

@ -36,6 +36,7 @@ import (
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1" supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
"code.icb4dc0.de/prskr/supabase-operator/assets/migrations" "code.icb4dc0.de/prskr/supabase-operator/assets/migrations"
"code.icb4dc0.de/prskr/supabase-operator/internal/db" "code.icb4dc0.de/prskr/supabase-operator/internal/db"
"code.icb4dc0.de/prskr/supabase-operator/internal/errx"
"code.icb4dc0.de/prskr/supabase-operator/internal/meta" "code.icb4dc0.de/prskr/supabase-operator/internal/meta"
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase" "code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
) )
@ -69,7 +70,7 @@ func (r *CoreDbReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
} }
defer CloseCtx(ctx, conn, &err) defer errx.CloseCtx(ctx, conn, &err)
logger.Info("Connected to database, checking for outstanding migrations") logger.Info("Connected to database, checking for outstanding migrations")
if err := r.applyMissingMigrations(ctx, conn, &core); err != nil { if err := r.applyMissingMigrations(ctx, conn, &core); err != nil {

View file

@ -68,7 +68,7 @@ var _ = Describe("Dashboard Controller", func() {
}) })
It("should successfully reconcile the resource", func() { It("should successfully reconcile the resource", func() {
By("Reconciling the created resource") By("Reconciling the created resource")
controllerReconciler := &DashboardReconciler{ controllerReconciler := &DashboardPGMetaReconciler{
Client: k8sClient, Client: k8sClient,
Scheme: k8sClient.Scheme(), Scheme: k8sClient.Scheme(),
} }

View file

@ -3,8 +3,6 @@ package controller
import ( import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"errors"
"io"
"maps" "maps"
"reflect" "reflect"
@ -20,17 +18,6 @@ import (
"code.icb4dc0.de/prskr/supabase-operator/api" "code.icb4dc0.de/prskr/supabase-operator/api"
) )
func Close(closer io.Closer, err *error) {
*err = errors.Join(*err, closer.Close())
}
func CloseCtx(ctx context.Context, closable interface {
Close(ctx context.Context) error
}, err *error,
) {
*err = errors.Join(*err, closable.Close(ctx))
}
func ptrOf[T any](val T) *T { func ptrOf[T any](val T) *T {
return &val return &val
} }

View file

@ -5,10 +5,10 @@ import (
"errors" "errors"
"iter" "iter"
"code.icb4dc0.de/prskr/supabase-operator/assets/migrations"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1" supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
"code.icb4dc0.de/prskr/supabase-operator/assets/migrations"
) )
type Migrator struct { type Migrator struct {

18
internal/errx/closing.go Normal file
View file

@ -0,0 +1,18 @@
package errx
import (
"context"
"errors"
"io"
)
func Close(closer io.Closer, err *error) {
*err = errors.Join(*err, closer.Close())
}
func CloseCtx(ctx context.Context, closable interface {
Close(ctx context.Context) error
}, err *error,
) {
*err = errors.Join(*err, closable.Close(ctx))
}

View file

@ -83,5 +83,4 @@ var _ = Describe("APIGateway Webhook", func() {
// Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
// }) // })
}) })
}) })

View file

@ -83,5 +83,4 @@ var _ = Describe("Core Webhook", func() {
// Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
// }) // })
}) })
}) })

View file

@ -104,6 +104,7 @@ func (v *CoreCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Ob
return warns, nil return warns, nil
} }
//nolint:unparam // keep signature for later
func (v *CoreCustomValidator) validateDb( func (v *CoreCustomValidator) validateDb(
ctx context.Context, ctx context.Context,
core *supabasev1alpha1.Core, core *supabasev1alpha1.Core,

View file

@ -1,10 +1,12 @@
package main package main
import ( import (
"archive/tar"
"compress/gzip"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io/fs" "io"
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
@ -13,8 +15,9 @@ import (
"strings" "strings"
"github.com/magefile/mage/mg" "github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"code.icb4dc0.de/prskr/supabase-operator/internal/errx"
) )
const ( const (
@ -50,7 +53,7 @@ func CRDDocs() error {
) )
} }
func FetchImageMeta(ctx context.Context) error { func FetchImageMeta(ctx context.Context) (err error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, composeFileUrl, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, composeFileUrl, nil)
if err != nil { if err != nil {
return err return err
@ -61,7 +64,7 @@ func FetchImageMeta(ctx context.Context) error {
return err return err
} }
defer resp.Body.Close() defer errx.Close(resp.Body, &err)
var composeFile struct { var composeFile struct {
Services map[string]struct { Services map[string]struct {
@ -78,7 +81,7 @@ func FetchImageMeta(ctx context.Context) error {
return err return err
} }
defer f.Close() defer errx.Close(f, &err)
type imageRef struct { type imageRef struct {
Repository string Repository string
@ -139,55 +142,75 @@ func FetchImageMeta(ctx context.Context) error {
return RunTool(tools[Gofumpt], "-l", "-w", f.Name()) return RunTool(tools[Gofumpt], "-l", "-w", f.Name())
} }
func FetchMigrations(ctx context.Context) error { func FetchMigrations(ctx context.Context) (err error) {
latestRelease, err := latestReleaseVersion(ctx, "supabase", "postgres") latestRelease, err := latestReleaseVersion(ctx, "supabase", "postgres")
if err != nil { if err != nil {
return err return err
} }
checkoutDir, err := os.MkdirTemp(os.TempDir(), "supabase-*") 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 { if err != nil {
return err return err
} }
repoFS := os.DirFS(checkoutDir) resp, err := http.DefaultClient.Do(req)
defer os.RemoveAll(checkoutDir)
if err := Git("clone", "--filter=blob:none", "--no-checkout", "https://github.com/supabase/postgres", checkoutDir); err != nil {
return err
}
if err := Git("-C", checkoutDir, "sparse-checkout", "set", "--cone", "migrations"); err != nil {
return err
}
if err := Git("-C", checkoutDir, "checkout", latestRelease); err != nil {
return err
}
migrationsDirPath := path.Join(".", "migrations", "db")
return fs.WalkDir(repoFS, migrationsDirPath, func(filePath string, d fs.DirEntry, err error) error {
if err != nil { if err != nil {
return err return err
} }
if d.IsDir() || filepath.Ext(filePath) != ".sql" { defer errx.Close(resp.Body, &err)
gzipReader, err := gzip.NewReader(resp.Body)
if err != nil {
return err
}
defer errx.Close(gzipReader, &err)
migrationsDirPath := path.Join(fmt.Sprintf("postgres-%s", latestRelease), ".", "migrations", "db") + "/"
tarReader := tar.NewReader(gzipReader)
var header *tar.Header
for header, err = tarReader.Next(); err == nil; header, err = tarReader.Next() {
fileInfo := header.FileInfo()
if fileInfo.IsDir() || path.Ext(fileInfo.Name()) != ".sql" {
continue
}
fileName := header.Name
if strings.HasPrefix(fileName, migrationsDirPath) {
fileName = strings.TrimPrefix(fileName, migrationsDirPath)
dir, _ := path.Split(fileName)
outDir := filepath.Join(workingDir, "assets", "migrations", filepath.FromSlash(dir))
if err := os.MkdirAll(outDir, 0o750); err != nil {
return err
}
slog.Info("Copying file", slog.String("file", fileName))
outFile, err := os.Create(filepath.Join(workingDir, "assets", "migrations", filepath.FromSlash(fileName)))
if err != nil {
return err
}
if _, err := io.Copy(outFile, tarReader); err != nil {
return err
}
if err := outFile.Close(); err != nil {
return err
}
} else {
slog.Debug("skipping file", slog.String("file", fileName))
}
}
if errors.Is(err, io.EOF) {
return nil return nil
} }
fileName, err := filepath.Rel(migrationsDirPath, filePath)
if err != nil {
return err return err
}
dir, _ := filepath.Split(fileName)
if err := os.MkdirAll(filepath.Join(workingDir, "assets", "migrations", dir), 0o750); err != nil {
return err
}
slog.Info("Copying migration file", slog.String("file", fileName))
return sh.Copy(filepath.Join(workingDir, "assets", "migrations", fileName), filepath.Join(checkoutDir, filepath.FromSlash(filePath)))
})
} }

View file

@ -5,10 +5,13 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"code.icb4dc0.de/prskr/supabase-operator/internal/errx"
) )
func latestReleaseVersion(ctx context.Context, owner, repo string) (string, error) { func latestReleaseVersion(ctx context.Context, owner, repo string) (tagName string, err error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo), nil) releaseURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, releaseURL, nil)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -18,7 +21,7 @@ func latestReleaseVersion(ctx context.Context, owner, repo string) (string, erro
return "", err return "", err
} }
defer resp.Body.Close() defer errx.Close(resp.Body, &err)
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("failed to retrieve latest release: %s", resp.Status) return "", fmt.Errorf("failed to retrieve latest release: %s", resp.Status)

View file

@ -1,54 +0,0 @@
package main
import (
"context"
"errors"
"log/slog"
"os"
"github.com/jackc/pgx/v5"
"code.icb4dc0.de/prskr/supabase-operator/assets/migrations"
)
func Migrate(ctx context.Context) error {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
return errors.New("DATABASE_URL is required")
}
conn, err := pgx.Connect(ctx, dsn)
if err != nil {
return err
}
defer conn.Close(ctx)
for s, err := range migrations.InitScripts() {
if err != nil {
return err
}
slog.Info("Running init script", slog.String("file", s.FileName))
_, err = conn.Exec(ctx, s.Content)
if err != nil {
return err
}
}
for s, err := range migrations.MigrationScripts() {
if err != nil {
return err
}
slog.Info("Running migration script", slog.String("file", s.FileName))
_, err = conn.Exec(ctx, s.Content)
if err != nil {
return err
}
}
return nil
}

View file

@ -316,7 +316,7 @@ func serviceAccountToken() (string, error) {
// Parse the JSON output to extract the token // Parse the JSON output to extract the token
var token tokenRequest var token tokenRequest
err = json.Unmarshal([]byte(output), &token) err = json.Unmarshal(output, &token)
g.Expect(err).NotTo(HaveOccurred()) g.Expect(err).NotTo(HaveOccurred())
out = token.Status.Token out = token.Status.Token

View file

@ -92,7 +92,7 @@ func IsPrometheusCRDsInstalled() bool {
if err != nil { if err != nil {
return false return false
} }
crdList := GetNonEmptyLines(string(output)) crdList := GetNonEmptyLines(output)
for _, crd := range prometheusCRDs { for _, crd := range prometheusCRDs {
for _, line := range crdList { for _, line := range crdList {
if strings.Contains(line, crd) { if strings.Contains(line, crd) {
@ -153,7 +153,7 @@ func IsCertManagerCRDsInstalled() bool {
} }
// Check if any of the Cert Manager CRDs are present // Check if any of the Cert Manager CRDs are present
crdList := GetNonEmptyLines(string(output)) crdList := GetNonEmptyLines(output)
for _, crd := range certManagerCRDs { for _, crd := range certManagerCRDs {
for _, line := range crdList { for _, line := range crdList {
if strings.Contains(line, crd) { if strings.Contains(line, crd) {