diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2df6491a5..60c4684b2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,22 +4,42 @@ updates: directory: "/" schedule: interval: "daily" + groups: + action-minor: + update-types: + - minor + - patch - package-ecosystem: "gomod" - directory: "/" - schedule: - interval: "daily" - - package-ecosystem: "gomod" - directory: "pkg" + directories: + - "/" + - "pkg" schedule: interval: "daily" + groups: + go-minor: + update-types: + - minor + - patch - package-ecosystem: "npm" directory: "/" schedule: interval: "daily" + groups: + npm-minor: + update-types: + - minor + - patch - package-ecosystem: "docker" directory: "pkg/config/templates" schedule: interval: "daily" + groups: + docker-minor: + exclude-patterns: + - supabase/logflare + update-types: + - minor + - patch ignore: - dependency-name: "library/kong" - dependency-name: "axllent/mailpit" diff --git a/.github/workflows/api-sync.yml b/.github/workflows/api-sync.yml index 764de088c..f07534a93 100644 --- a/.github/workflows/api-sync.yml +++ b/.github/workflows/api-sync.yml @@ -15,7 +15,7 @@ jobs: name: Sync API Types runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8f733605..849425404 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: name: Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: @@ -28,6 +28,20 @@ jobs: go tool gotestsum -- -race -v -count=1 ./... \ -coverpkg="./cmd/...,./internal/...,${pkgs}" -coverprofile=coverage.out + - uses: actions/upload-artifact@v4 + with: + name: code-coverage-report + path: coverage.out + + coverage: + name: Coverage + needs: + - test + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v5 + with: + name: code-coverage-report - uses: coverallsapp/github-action@v2 with: file: coverage.out @@ -37,7 +51,7 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: @@ -53,7 +67,7 @@ jobs: name: Start runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: go-version-file: go.mod @@ -69,7 +83,7 @@ jobs: name: Codegen runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 218e31608..e518cf1e5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -56,7 +56,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ace9fc072..b58e93c7f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,7 +14,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - run: gh pr create -B main -H develop --title 'Prod deploy' --fill diff --git a/.github/workflows/fast-forward.yml b/.github/workflows/fast-forward.yml index c4e78c241..08745d108 100644 --- a/.github/workflows/fast-forward.yml +++ b/.github/workflows/fast-forward.yml @@ -16,7 +16,7 @@ jobs: github.event.review.state == 'approved' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - run: | diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index b89594f8a..b3c6b8414 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -21,7 +21,7 @@ jobs: pack: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - run: | jq -c '.version = "1.28.0"' package.json > tmp.$$.json diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index d4054b3b3..55652c899 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -30,7 +30,7 @@ jobs: tags: ${{ steps.list.outputs.tags }} curr: ${{ steps.curr.outputs.tags }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: go-version-file: go.mod diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index e658a7614..93dab391b 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -20,7 +20,7 @@ jobs: new-release-version: ${{ steps.semantic-release.outputs.new_release_version }} new-release-channel: ${{ steps.semantic-release.outputs.new_release_channel }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - id: semantic-release uses: cycjimmy/semantic-release-action@v4 env: @@ -35,7 +35,7 @@ jobs: contents: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 @@ -65,7 +65,7 @@ jobs: if: needs.release.outputs.new-release-published == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: go-version-file: go.mod @@ -82,7 +82,7 @@ jobs: if: needs.release.outputs.new-release-published == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: "16.x" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 369f0de89..616738e90 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: outputs: release_tag: ${{ steps.prerelease.outputs.tagName }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - id: prerelease run: | gh release list --limit 1 --json tagName --jq \ @@ -31,7 +31,7 @@ jobs: - settings runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: go-version-file: go.mod @@ -53,7 +53,7 @@ jobs: name: Bump self-hosted versions runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: go-version-file: go.mod @@ -69,7 +69,7 @@ jobs: - publish runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: go-version-file: go.mod @@ -85,7 +85,7 @@ jobs: - settings runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: go-version-file: go.mod diff --git a/.github/workflows/tag-npm.yml b/.github/workflows/tag-npm.yml index 578756261..234c74ac9 100644 --- a/.github/workflows/tag-npm.yml +++ b/.github/workflows/tag-npm.yml @@ -21,7 +21,7 @@ jobs: name: Move latest tag runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: "16.x" diff --git a/cmd/db.go b/cmd/db.go index ae275e3ab..cc8bca65b 100644 --- a/cmd/db.go +++ b/cmd/db.go @@ -101,6 +101,8 @@ var ( if usePgSchema { differ = diff.DiffPgSchema fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "--use-pg-schema flag is experimental and may not include all entities, such as views and grants.") + } else if !viper.GetBool("EXPERIMENTAL") { + differ = diff.DiffSchemaMigraBash } return diff.Run(cmd.Context(), schema, file, flags.DbConfig, differ, afero.NewOsFs()) }, diff --git a/cmd/gen.go b/cmd/gen.go index 48a800f17..8893988d4 100644 --- a/cmd/gen.go +++ b/cmd/gen.go @@ -13,6 +13,7 @@ import ( "github.com/supabase/cli/internal/gen/types" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/config" ) var ( @@ -97,7 +98,7 @@ var ( algorithm = utils.EnumFlag{ Allowed: signingkeys.GetSupportedAlgorithms(), - Value: string(signingkeys.AlgES256), + Value: string(config.AlgES256), } appendKeys bool diff --git a/cmd/root.go b/cmd/root.go index 99b11d23d..db1cfabbc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -92,13 +92,16 @@ var ( return errors.New("must set the --experimental flag to run this command") } cmd.SilenceUsage = true - // Change workdir + // Load profile before changing workdir + ctx := cmd.Context() fsys := afero.NewOsFs() + if err := utils.LoadProfile(ctx, fsys); err != nil { + return err + } if err := utils.ChangeWorkDir(fsys); err != nil { return err } // Add common flags - ctx := cmd.Context() if IsManagementAPI(cmd) { if err := promptLogin(fsys); err != nil { return err @@ -236,6 +239,7 @@ func init() { flags.String("workdir", "", "path to a Supabase project directory") flags.Bool("experimental", false, "enable experimental features") flags.String("network-id", "", "use the specified docker network instead of a generated one") + flags.String("profile", "supabase", "use a specific profile for connecting to Supabase API") flags.VarP(&utils.OutputFormat, "output", "o", "output format of status variables") flags.Var(&utils.DNSResolver, "dns-resolver", "lookup domain names using the specified resolver") flags.BoolVar(&createTicket, "create-ticket", false, "create a support ticket for any CLI error") diff --git a/go.mod b/go.mod index 742d09f32..d7205b6a0 100644 --- a/go.mod +++ b/go.mod @@ -7,19 +7,20 @@ require ( github.com/Netflix/go-env v0.1.2 github.com/andybalholm/brotli v1.2.0 github.com/cenkalti/backoff/v4 v4.3.0 - github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.25.0 - github.com/charmbracelet/glamour v0.9.1 - github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.6 + github.com/charmbracelet/glamour v0.10.0 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/containerd/errdefs v1.0.0 github.com/containers/common v0.64.1 github.com/docker/cli v28.3.3+incompatible github.com/docker/docker v28.3.3+incompatible github.com/docker/go-connections v0.6.0 github.com/fsnotify/fsnotify v1.9.0 - github.com/getsentry/sentry-go v0.35.0 + github.com/getsentry/sentry-go v0.35.1 github.com/go-errors/errors v1.5.1 github.com/go-git/go-git/v5 v5.16.2 + github.com/go-playground/validator/v10 v10.27.0 github.com/go-xmlfmt/xmlfmt v1.1.3 github.com/google/go-github/v62 v62.0.0 github.com/google/go-querystring v1.1.0 @@ -38,8 +39,8 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.7 github.com/spf13/viper v1.20.1 - github.com/stretchr/testify v1.10.0 - github.com/stripe/pg-schema-diff v0.9.1 + github.com/stretchr/testify v1.11.0 + github.com/stripe/pg-schema-diff v0.9.2 github.com/supabase/cli/pkg v1.0.0 github.com/tidwall/jsonc v0.3.2 github.com/withfig/autocomplete-tools/packages/cobra v1.2.0 @@ -48,7 +49,7 @@ require ( golang.org/x/mod v0.27.0 golang.org/x/oauth2 v0.30.0 golang.org/x/term v0.34.0 - google.golang.org/grpc v1.74.2 + google.golang.org/grpc v1.75.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -96,13 +97,13 @@ require ( github.com/charithe/durationcheck v0.0.10 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.8.0 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/ansi v0.9.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/chavacava/garif v0.1.0 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect - github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containers/storage v1.59.1 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect @@ -122,6 +123,7 @@ require ( github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/ecies/go/v2 v2.0.11 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/ethereum/go-ethereum v1.15.8 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.18.0 // indirect @@ -130,6 +132,7 @@ require ( github.com/firefart/nonamedreturns v1.0.6 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/getkin/kin-openapi v0.131.0 // indirect github.com/ghostiam/protogetter v0.3.15 // indirect github.com/go-critic/go-critic v0.13.0 // indirect @@ -139,6 +142,8 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect @@ -205,6 +210,7 @@ require ( github.com/ldez/grignotin v0.9.0 // indirect github.com/ldez/tagliatelle v0.7.1 // indirect github.com/ldez/usetesting v0.4.3 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect github.com/lib/pq v1.10.9 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -234,7 +240,7 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/moricho/tparallel v0.3.2 // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/nakabonne/nestif v0.3.1 // indirect @@ -269,7 +275,7 @@ require ( github.com/ryancurrah/gomodguard v1.4.1 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect @@ -322,8 +328,8 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/sdk v1.36.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/atomic v1.9.0 // indirect @@ -339,8 +345,8 @@ require ( golang.org/x/tools v0.35.0 // indirect golang.org/x/tools/go/expect v0.1.1-deprecated // indirect golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 0e33afdb9..0e9329b7e 100644 --- a/go.sum +++ b/go.sum @@ -157,24 +157,26 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= -github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= -github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= -github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= -github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM= -github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk= +github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= +github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= -github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= @@ -191,8 +193,6 @@ github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -271,6 +271,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/ethereum/go-ethereum v1.15.8 h1:H6NilvRXFVoHiXZ3zkuTqKW5XcxjLZniV5UjxJt1GJU= github.com/ethereum/go-ethereum v1.15.8/go.mod h1:+S9k+jFzlyVTNcYGvqFhzN/SFhI6vA+aOY4T5tLSPL0= @@ -294,10 +296,12 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE= github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= -github.com/getsentry/sentry-go v0.35.0 h1:+FJNlnjJsZMG3g0/rmmP7GiKjQoUF5EXfEtBwtPtkzY= -github.com/getsentry/sentry-go v0.35.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= +github.com/getsentry/sentry-go v0.35.1 h1:iopow6UVLE2aXu46xKVIs8Z9D/YZkJrHkgozrxa+tOQ= +github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= github.com/ghostiam/protogetter v0.3.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY= github.com/ghostiam/protogetter v0.3.15/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= @@ -332,6 +336,14 @@ github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1 github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -639,6 +651,8 @@ github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORI github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I= github.com/ldez/usetesting v0.4.3 h1:pJpN0x3fMupdTf/IapYjnkhiY1nSTN+pox1/GyBRw3k= github.com/ldez/usetesting v0.4.3/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -725,8 +739,8 @@ github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKH github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= @@ -872,8 +886,8 @@ github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9f github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= -github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= @@ -953,10 +967,10 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stripe/pg-schema-diff v0.9.1 h1:vF8PBxgrTGSxy2qOgRrcjnHGh1rQmE3ZALJNO3Juc8Y= -github.com/stripe/pg-schema-diff v0.9.1/go.mod h1:cl2VC6te/cCTOewTRvv4pYsgQqAOhvRQmatCHfYwy8c= +github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= +github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stripe/pg-schema-diff v0.9.2 h1:Xn0DX2/u46nNTR0u4ATbTkDf6DmruZjSTdHJFkhbcSU= +github.com/stripe/pg-schema-diff v0.9.2/go.mod h1:cl2VC6te/cCTOewTRvv4pYsgQqAOhvRQmatCHfYwy8c= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8= @@ -1059,10 +1073,10 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3S go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= @@ -1282,6 +1296,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1289,7 +1304,6 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1408,6 +1422,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1459,10 +1475,10 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1476,8 +1492,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= -google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/internal/bootstrap/bootstrap_test.go b/internal/bootstrap/bootstrap_test.go index ab12d1268..fac610419 100644 --- a/internal/bootstrap/bootstrap_test.go +++ b/internal/bootstrap/bootstrap_test.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" "github.com/supabase/cli/pkg/api" ) @@ -64,6 +65,7 @@ func TestWriteEnv(t *testing.T) { t.Run("writes .env", func(t *testing.T) { flags.ProjectRef = "testing" + utils.CurrentProfile.ProjectHost = "supabase.co" // Setup in-memory fs fsys := afero.NewMemMapFs() // Run test @@ -80,6 +82,7 @@ SUPABASE_URL="https://testing.supabase.co"`, string(env)) t.Run("merges with .env.example", func(t *testing.T) { flags.ProjectRef = "testing" + utils.CurrentProfile.ProjectHost = "supabase.co" // Setup in-memory fs fsys := afero.NewMemMapFs() example, err := godotenv.Marshal(map[string]string{ diff --git a/internal/branches/list/list.go b/internal/branches/list/list.go index 4807c7059..0d2335162 100644 --- a/internal/branches/list/list.go +++ b/internal/branches/list/list.go @@ -22,8 +22,8 @@ func Run(ctx context.Context, fsys afero.Fs) error { switch utils.OutputFormat.Value { case utils.OutputPretty: - table := `|ID|NAME|DEFAULT|GIT BRANCH|STATUS|CREATED AT (UTC)|UPDATED AT (UTC)| -|-|-|-|-|-|-|-| + table := `|ID|BRANCH PROJECT ID|NAME|DEFAULT|GIT BRANCH|STATUS|CREATED AT (UTC)|UPDATED AT (UTC)| +|-|-|-|-|-|-|-|-| ` for _, branch := range branches { gitBranch := " " @@ -31,8 +31,9 @@ func Run(ctx context.Context, fsys afero.Fs) error { gitBranch = *branch.GitBranch } table += fmt.Sprintf( - "|`%s`|`%s`|`%t`|`%s`|`%s`|`%s`|`%s`|\n", + "|`%s`|`%s`|`%s`|`%t`|`%s`|`%s`|`%s`|`%s`|\n", branch.Id, + branch.ProjectRef, strings.ReplaceAll(branch.Name, "|", "\\|"), branch.IsDefault, strings.ReplaceAll(gitBranch, "|", "\\|"), diff --git a/internal/db/diff/diff.go b/internal/db/diff/diff.go index 761e94193..e34dfbcb4 100644 --- a/internal/db/diff/diff.go +++ b/internal/db/diff/diff.go @@ -28,7 +28,7 @@ import ( "github.com/supabase/cli/pkg/parser" ) -type DiffFunc func(context.Context, string, string, []string) (string, error) +type DiffFunc func(context.Context, string, string, []string, ...func(*pgx.ConnConfig)) (string, error) func Run(ctx context.Context, schema []string, file string, config pgconn.Config, differ DiffFunc, fsys afero.Fs, options ...func(*pgx.ConnConfig)) (err error) { out, err := DiffDatabase(ctx, schema, config, os.Stderr, fsys, differ, options...) @@ -136,7 +136,7 @@ func MigrateShadowDatabase(ctx context.Context, container string, fsys afero.Fs, return migration.ApplyMigrations(ctx, migrations, conn, afero.NewIOFS(fsys)) } -func DiffDatabase(ctx context.Context, schema []string, config pgconn.Config, w io.Writer, fsys afero.Fs, differ func(context.Context, string, string, []string) (string, error), options ...func(*pgx.ConnConfig)) (string, error) { +func DiffDatabase(ctx context.Context, schema []string, config pgconn.Config, w io.Writer, fsys afero.Fs, differ DiffFunc, options ...func(*pgx.ConnConfig)) (string, error) { fmt.Fprintln(w, "Creating shadow database...") shadow, err := CreateShadowDatabase(ctx, utils.Config.Db.ShadowPort) if err != nil { @@ -175,7 +175,7 @@ func DiffDatabase(ctx context.Context, schema []string, config pgconn.Config, w } source := utils.ToPostgresURL(shadowConfig) target := utils.ToPostgresURL(config) - return differ(ctx, source, target, schema) + return differ(ctx, source, target, schema, options...) } func migrateBaseDatabase(ctx context.Context, config pgconn.Config, migrations []string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { diff --git a/internal/db/diff/diff_test.go b/internal/db/diff/diff_test.go index 22fae53df..9a708a1c4 100644 --- a/internal/db/diff/diff_test.go +++ b/internal/db/diff/diff_test.go @@ -15,6 +15,7 @@ import ( "github.com/h2non/gock" "github.com/jackc/pgconn" "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v4" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -72,7 +73,17 @@ func TestRun(t *testing.T) { Reply("CREATE DATABASE") defer conn.Close(t) // Run test - err := Run(context.Background(), []string{"public"}, "file", dbConfig, DiffSchemaMigra, fsys, conn.Intercept) + err := Run(context.Background(), []string{"public"}, "file", dbConfig, DiffSchemaMigra, fsys, func(cc *pgx.ConnConfig) { + if cc.Host == dbConfig.Host { + // Fake a SSL error when connecting to target database + cc.LookupFunc = func(ctx context.Context, host string) (addrs []string, err error) { + return nil, errors.New("server refused TLS connection") + } + } else { + // Hijack connection to shadow database + conn.Intercept(cc) + } + }) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -306,7 +317,17 @@ create schema public`) Query(migration.INSERT_MIGRATION_VERSION, "0", "test", []string{sql}). Reply("INSERT 0 1") // Run test - diff, err := DiffDatabase(context.Background(), []string{"public"}, dbConfig, io.Discard, fsys, DiffSchemaMigra, conn.Intercept) + diff, err := DiffDatabase(context.Background(), []string{"public"}, dbConfig, io.Discard, fsys, DiffSchemaMigra, func(cc *pgx.ConnConfig) { + if cc.Host == dbConfig.Host { + // Fake a SSL error when connecting to target database + cc.LookupFunc = func(ctx context.Context, host string) (addrs []string, err error) { + return nil, errors.New("server refused TLS connection") + } + } else { + // Hijack connection to shadow database + conn.Intercept(cc) + } + }) // Check error assert.Empty(t, diff) assert.ErrorContains(t, err, "error diffing schema") diff --git a/internal/db/diff/migra.go b/internal/db/diff/migra.go index 49ba9c447..63e0566fd 100644 --- a/internal/db/diff/migra.go +++ b/internal/db/diff/migra.go @@ -9,9 +9,12 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/go-errors/errors" + "github.com/jackc/pgx/v4" "github.com/spf13/viper" + "github.com/supabase/cli/internal/gen/types" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/config" + "github.com/supabase/cli/pkg/migration" ) var ( @@ -20,6 +23,11 @@ var ( //go:embed templates/migra.ts diffSchemaTypeScript string + //go:embed templates/staging-ca-2021.crt + caStaging string + //go:embed templates/prod-ca-2021.crt + caProd string + managedSchemas = []string{ // Local development "_analytics", @@ -54,7 +62,14 @@ var ( ) // Diffs local database schema against shadow, dumps output to stdout. -func DiffSchemaMigraBash(ctx context.Context, source, target string, schema []string) (string, error) { +func DiffSchemaMigraBash(ctx context.Context, source, target string, schema []string, options ...func(*pgx.ConnConfig)) (string, error) { + // Load all user defined schemas + if len(schema) == 0 { + var err error + if schema, err = loadSchema(ctx, target, options...); err != nil { + return "", err + } + } env := []string{"SOURCE=" + source, "TARGET=" + target} // Passing in script string means command line args must be set manually, ie. "$@" args := "set -- " + strings.Join(schema, " ") + ";" @@ -80,8 +95,25 @@ func DiffSchemaMigraBash(ctx context.Context, source, target string, schema []st return out.String(), nil } -func DiffSchemaMigra(ctx context.Context, source, target string, schema []string) (string, error) { +func loadSchema(ctx context.Context, dbURL string, options ...func(*pgx.ConnConfig)) ([]string, error) { + conn, err := utils.ConnectByUrl(ctx, dbURL, options...) + if err != nil { + return nil, err + } + defer conn.Close(context.Background()) + // RLS policies in auth and storage schemas can be included with -s flag + return migration.ListUserSchemas(ctx, conn) +} + +func DiffSchemaMigra(ctx context.Context, source, target string, schema []string, options ...func(*pgx.ConnConfig)) (string, error) { env := []string{"SOURCE=" + source, "TARGET=" + target} + // node-postgres does not support sslmode=prefer + if require, err := types.IsRequireSSL(ctx, target, options...); err != nil { + return "", err + } else if require { + rootCA := caStaging + caProd + env = append(env, "SSL_CA="+rootCA) + } if len(schema) > 0 { env = append(env, "INCLUDED_SCHEMAS="+strings.Join(schema, ",")) } else { diff --git a/internal/db/diff/pgschema.go b/internal/db/diff/pgschema.go index 8967f7ad1..36387990c 100644 --- a/internal/db/diff/pgschema.go +++ b/internal/db/diff/pgschema.go @@ -7,10 +7,11 @@ import ( "strings" "github.com/go-errors/errors" + "github.com/jackc/pgx/v4" pgschema "github.com/stripe/pg-schema-diff/pkg/diff" ) -func DiffPgSchema(ctx context.Context, source, target string, schema []string) (string, error) { +func DiffPgSchema(ctx context.Context, source, target string, schema []string, _ ...func(*pgx.ConnConfig)) (string, error) { dbSrc, err := sql.Open("pgx", source) if err != nil { return "", errors.Errorf("failed to open source database: %w", err) diff --git a/internal/db/diff/templates/migra.ts b/internal/db/diff/templates/migra.ts index 91d21701f..bf915cded 100644 --- a/internal/db/diff/templates/migra.ts +++ b/internal/db/diff/templates/migra.ts @@ -1,8 +1,12 @@ import { createClient } from "npm:@pgkit/client"; import { Migration } from "npm:@pgkit/migra"; +// Avoids error on self-signed certificate +const ca = Deno.env.get("SSL_CA"); const clientBase = createClient(Deno.env.get("SOURCE")); -const clientHead = createClient(Deno.env.get("TARGET")); +const clientHead = createClient(Deno.env.get("TARGET"), { + pgpOptions: { connect: { ssl: ca && { ca } } }, +}); const includedSchemas = Deno.env.get("INCLUDED_SCHEMAS")?.split(",") ?? []; const excludedSchemas = Deno.env.get("EXCLUDED_SCHEMAS")?.split(",") ?? []; diff --git a/internal/db/diff/templates/prod-ca-2021.crt b/internal/db/diff/templates/prod-ca-2021.crt new file mode 100644 index 000000000..3d693669b --- /dev/null +++ b/internal/db/diff/templates/prod-ca-2021.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDxDCCAqygAwIBAgIUbLxMod62P2ktCiAkxnKJwtE9VPYwDQYJKoZIhvcNAQEL +BQAwazELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5l +dyBDYXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJh +c2UgUm9vdCAyMDIxIENBMB4XDTIxMDQyODEwNTY1M1oXDTMxMDQyNjEwNTY1M1ow +azELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5ldyBD +YXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJhc2Ug +Um9vdCAyMDIxIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQXW +QyHOB+qR2GJobCq/CBmQ40G0oDmCC3mzVnn8sv4XNeWtE5XcEL0uVih7Jo4Dkx1Q +DmGHBH1zDfgs2qXiLb6xpw/CKQPypZW1JssOTMIfQppNQ87K75Ya0p25Y3ePS2t2 +GtvHxNjUV6kjOZjEn2yWEcBdpOVCUYBVFBNMB4YBHkNRDa/+S4uywAoaTWnCJLUi +cvTlHmMw6xSQQn1UfRQHk50DMCEJ7Cy1RxrZJrkXXRP3LqQL2ijJ6F4yMfh+Gyb4 +O4XajoVj/+R4GwywKYrrS8PrSNtwxr5StlQO8zIQUSMiq26wM8mgELFlS/32Uclt +NaQ1xBRizkzpZct9DwIDAQABo2AwXjALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFKjX +uXY32CztkhImng4yJNUtaUYsMB8GA1UdIwQYMBaAFKjXuXY32CztkhImng4yJNUt +aUYsMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAB8spzNn+4VU +tVxbdMaX+39Z50sc7uATmus16jmmHjhIHz+l/9GlJ5KqAMOx26mPZgfzG7oneL2b +VW+WgYUkTT3XEPFWnTp2RJwQao8/tYPXWEJDc0WVQHrpmnWOFKU/d3MqBgBm5y+6 +jB81TU/RG2rVerPDWP+1MMcNNy0491CTL5XQZ7JfDJJ9CCmXSdtTl4uUQnSuv/Qx +Cea13BX2ZgJc7Au30vihLhub52De4P/4gonKsNHYdbWjg7OWKwNv/zitGDVDB9Y2 +CMTyZKG3XEu5Ghl1LEnI3QmEKsqaCLv12BnVjbkSeZsMnevJPs1Ye6TjjJwdik5P +o/bKiIz+Fq8= +-----END CERTIFICATE----- diff --git a/internal/db/diff/templates/staging-ca-2021.crt b/internal/db/diff/templates/staging-ca-2021.crt new file mode 100644 index 000000000..925f9f5af --- /dev/null +++ b/internal/db/diff/templates/staging-ca-2021.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID1DCCArygAwIBAgIUbYRdq/8/uNq8G9stMCdOFSBgA2MwDQYJKoZIhvcNAQEL +BQAwczELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5l +dyBDYXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEmMCQGA1UEAwwdU3VwYWJh +c2UgU3RhZ2luZyBSb290IDIwMjEgQ0EwHhcNMjEwNDI4MTAzNjEzWhcNMzEwNDI2 +MTAzNjEzWjBzMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRGVsd2FyZTETMBEGA1UE +BwwKTmV3IENhc3RsZTEVMBMGA1UECgwMU3VwYWJhc2UgSW5jMSYwJAYDVQQDDB1T +dXBhYmFzZSBTdGFnaW5nIFJvb3QgMjAyMSBDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAN0AKRE8a56O8LaZxiOAcHFUFnwiKUvPoXPq26Ifw+Nv+7zg +N2V5WnMZbbw24q61Os60ZUn0XmbVtuIeJ+stPHsO7qxxuL+bmPR+qU5tkDrIOyEe +YD/2u8/q6ssVv42k4XcXbhM6RVz7CkCDY0TiBm1bMtRZso3xB6E9wAjxDf43XfV5 +PAGs3JI+Zo/vyqCDlN0hHOrB/aBl01JXqQWI84Gia5ooucq4SjA1CyawBcQ2IAvG +rXuy1BouY+xM3zRuNvtfFP6rb5Mta+jCYEMh1AZ8yP8sYUWAyhxX6k9EbOb009wQ +aZljbUCh/UglGWuBxdzePavx+zPjzWXB1NyVkpkCAwEAAaNgMF4wCwYDVR0PBAQD +AgEGMB0GA1UdDgQWBBQFx+PHLf27iIo/PMfIfGqXF7Zb+DAfBgNVHSMEGDAWgBQF +x+PHLf27iIo/PMfIfGqXF7Zb+DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB +CwUAA4IBAQB/xIiz5dDqzGXjqYqXZYx4iSfSxsVayeOPDMfmaiCfSMJEUG4cUiwG +OvMPGztaUEYeip5SCvSKuAAjVkXyP7ahKR7t7lZ9mErVXyxSZoVLbOd578CuYiZk +OgT17UjPv66WMzEKEr8wGpomTYWWfEkuqt8ENdiM1Z4LNFahdKj36+jm6/a+9R8K +25VIL68DTaQpBxFWG6ixC1HRMHJ12lDhKsshIi099BVpkGibESlxPrQOdKKqBB/J +vIX+/Hb+mS4H5zYMeK2wX0onp+GBcD6X9L1UJuXMVd+BRan8RFidXL5s3++xXjQq +Nzbc6lnA69urKffvcT07YwMsY/OmHzVa +-----END CERTIFICATE----- diff --git a/internal/db/pull/pull.go b/internal/db/pull/pull.go index 9c302e61b..3363ce7a6 100644 --- a/internal/db/pull/pull.go +++ b/internal/db/pull/pull.go @@ -44,9 +44,7 @@ func Run(ctx context.Context, schema []string, config pgconn.Config, name string // 2. Pull schema timestamp := utils.GetCurrentTimestamp() path := new.GetMigrationPath(timestamp, name) - if err := utils.RunProgram(ctx, func(p utils.Program, ctx context.Context) error { - return run(p, ctx, schema, path, conn, fsys) - }); err != nil { + if err := run(ctx, schema, path, conn, fsys); err != nil { return err } // 3. Insert a row to `schema_migrations` @@ -59,12 +57,12 @@ func Run(ctx context.Context, schema []string, config pgconn.Config, name string return nil } -func run(p utils.Program, ctx context.Context, schema []string, path string, conn *pgx.Conn, fsys afero.Fs) error { +func run(ctx context.Context, schema []string, path string, conn *pgx.Conn, fsys afero.Fs) error { config := conn.Config().Config // 1. Assert `supabase/migrations` and `schema_migrations` are in sync. if err := assertRemoteInSync(ctx, conn, fsys); errors.Is(err, errMissing) { // Not passing down schemas to avoid pulling in managed schemas - if err = dumpRemoteSchema(p, ctx, path, config, fsys); err == nil { + if err = dumpRemoteSchema(ctx, path, config, fsys); err == nil { utils.CmdSuggestion = suggestExtraPull } return err @@ -80,16 +78,16 @@ func run(p utils.Program, ctx context.Context, schema []string, path string, con return err } } - err := diffRemoteSchema(p, ctx, schema, path, config, fsys) + err := diffRemoteSchema(ctx, schema, path, config, fsys) if defaultSchema && (err == nil || errors.Is(err, errInSync)) { utils.CmdSuggestion = suggestExtraPull } return err } -func dumpRemoteSchema(p utils.Program, ctx context.Context, path string, config pgconn.Config, fsys afero.Fs) error { +func dumpRemoteSchema(ctx context.Context, path string, config pgconn.Config, fsys afero.Fs) error { // Special case if this is the first migration - p.Send(utils.StatusMsg("Dumping schema from remote database...")) + fmt.Fprintln(os.Stderr, "Dumping schema from remote database...") if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(path)); err != nil { return err } @@ -101,10 +99,9 @@ func dumpRemoteSchema(p utils.Program, ctx context.Context, path string, config return migration.DumpSchema(ctx, config, f, dump.DockerExec) } -func diffRemoteSchema(p utils.Program, ctx context.Context, schema []string, path string, config pgconn.Config, fsys afero.Fs) error { - w := utils.StatusWriter{Program: p} +func diffRemoteSchema(ctx context.Context, schema []string, path string, config pgconn.Config, fsys afero.Fs) error { // Diff remote db (source) & shadow db (target) and write it as a new migration. - output, err := diff.DiffDatabase(ctx, schema, config, w, fsys, diff.DiffSchemaMigra) + output, err := diff.DiffDatabase(ctx, schema, config, os.Stderr, fsys, diff.DiffSchemaMigra) if err != nil { return err } diff --git a/internal/db/pull/pull_test.go b/internal/db/pull/pull_test.go index d156db9b0..33e1d849f 100644 --- a/internal/db/pull/pull_test.go +++ b/internal/db/pull/pull_test.go @@ -70,9 +70,7 @@ func TestPullSchema(t *testing.T) { conn.Query(migration.LIST_MIGRATION_VERSION). Reply("SELECT 0") // Run test - err := utils.RunProgram(context.Background(), func(p utils.Program, ctx context.Context) error { - return run(p, ctx, nil, "0_test.sql", conn.MockClient(t), fsys) - }) + err := run(context.Background(), nil, "0_test.sql", conn.MockClient(t), fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -94,9 +92,7 @@ func TestPullSchema(t *testing.T) { Query(migration.ListSchemas, migration.ManagedSchemas). ReplyError(pgerrcode.DuplicateTable, `relation "test" already exists`) // Run test - err := utils.RunProgram(context.Background(), func(p utils.Program, ctx context.Context) error { - return run(p, ctx, nil, "", conn.MockClient(t), fsys) - }) + err := run(context.Background(), nil, "", conn.MockClient(t), fsys) // Check error assert.ErrorContains(t, err, `ERROR: relation "test" already exists (SQLSTATE 42P07)`) }) @@ -118,9 +114,7 @@ func TestPullSchema(t *testing.T) { conn.Query(migration.LIST_MIGRATION_VERSION). Reply("SELECT 1", []interface{}{"0"}) // Run test - err := utils.RunProgram(context.Background(), func(p utils.Program, ctx context.Context) error { - return run(p, ctx, []string{"public"}, "", conn.MockClient(t), fsys) - }) + err := run(context.Background(), []string{"public"}, "", conn.MockClient(t), fsys) // Check error assert.ErrorContains(t, err, "network error") assert.Empty(t, apitest.ListUnmatchedRequests()) diff --git a/internal/db/start/start.go b/internal/db/start/start.go index 665c5f4f7..7febc9294 100644 --- a/internal/db/start/start.go +++ b/internal/db/start/start.go @@ -73,7 +73,7 @@ func NewContainerConfig(args ...string) container.Config { } if len(utils.Config.Experimental.OrioleDBVersion) > 0 { env = append(env, - "POSTGRES_INITDB_ARGS=--lc-collate=C", + "POSTGRES_INITDB_ARGS=--lc-collate=C --lc-ctype=C", fmt.Sprintf("S3_ENABLED=%t", true), "S3_HOST="+utils.Config.Experimental.S3Host, "S3_REGION="+utils.Config.Experimental.S3Region, diff --git a/internal/functions/list/list_test.go b/internal/functions/list/list_test.go index e9c7f26b7..bbc704f9d 100644 --- a/internal/functions/list/list_test.go +++ b/internal/functions/list/list_test.go @@ -3,6 +3,7 @@ package list import ( "context" "errors" + "net/http" "testing" "github.com/h2non/gock" @@ -14,14 +15,15 @@ import ( ) func TestFunctionsListCommand(t *testing.T) { + // Setup valid project ref + project := apitest.RandomProjectRef() + // Setup valid access token + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + t.Run("lists all functions", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() - // Setup valid project ref - project := apitest.RandomProjectRef() - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) // Flush pending mocks after test execution defer gock.OffAll() @@ -53,11 +55,16 @@ func TestFunctionsListCommand(t *testing.T) { assert.Empty(t, apitest.ListUnmatchedRequests()) }) - t.Run("throws error on missing access token", func(t *testing.T) { + t.Run("throws error on service unavailable", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() + // Flush pending mocks after test execution + defer gock.OffAll() + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/functions"). + Reply(http.StatusServiceUnavailable) // Run test - err := Run(context.Background(), "", fsys) + err := Run(context.Background(), project, fsys) // Check error assert.ErrorContains(t, err, "Unexpected error retrieving functions") }) @@ -65,11 +72,6 @@ func TestFunctionsListCommand(t *testing.T) { t.Run("throws error on network error", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() - // Setup valid project ref - project := apitest.RandomProjectRef() - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) // Flush pending mocks after test execution defer gock.OffAll() gock.New(utils.DefaultApiHost). diff --git a/internal/gen/signingkeys/signingkeys.go b/internal/gen/signingkeys/signingkeys.go index 7394a99c4..22946df67 100644 --- a/internal/gen/signingkeys/signingkeys.go +++ b/internal/gen/signingkeys/signingkeys.go @@ -20,58 +20,24 @@ import ( "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" "github.com/supabase/cli/pkg/cast" + "github.com/supabase/cli/pkg/config" ) -type Algorithm string - -const ( - AlgRS256 Algorithm = "RS256" - AlgES256 Algorithm = "ES256" -) - -type JWK struct { - KeyType string `json:"kty"` - KeyID string `json:"kid,omitempty"` - Use string `json:"use,omitempty"` - KeyOps []string `json:"key_ops,omitempty"` - Algorithm string `json:"alg,omitempty"` - Extractable *bool `json:"ext,omitempty"` - // RSA specific fields - Modulus string `json:"n,omitempty"` - Exponent string `json:"e,omitempty"` - // RSA private key fields - PrivateExponent string `json:"d,omitempty"` - FirstPrimeFactor string `json:"p,omitempty"` - SecondPrimeFactor string `json:"q,omitempty"` - FirstFactorCRTExponent string `json:"dp,omitempty"` - SecondFactorCRTExponent string `json:"dq,omitempty"` - FirstCRTCoefficient string `json:"qi,omitempty"` - // EC specific fields - Curve string `json:"crv,omitempty"` - X string `json:"x,omitempty"` - Y string `json:"y,omitempty"` -} - -type KeyPair struct { - PublicKey JWK - PrivateKey JWK -} - -// GenerateKeyPair generates a new key pair for the specified algorithm -func GenerateKeyPair(alg Algorithm) (*KeyPair, error) { - keyID := uuid.New().String() +// GeneratePrivateKey generates a new private key for the specified algorithm +func GeneratePrivateKey(alg config.Algorithm) (*config.JWK, error) { + keyID := uuid.New() switch alg { - case AlgRS256: + case config.AlgRS256: return generateRSAKeyPair(keyID) - case AlgES256: + case config.AlgES256: return generateECDSAKeyPair(keyID) default: return nil, errors.Errorf("unsupported algorithm: %s", alg) } } -func generateRSAKeyPair(keyID string) (*KeyPair, error) { +func generateRSAKeyPair(keyID uuid.UUID) (*config.JWK, error) { // Generate RSA key pair (2048 bits for RS256) privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { @@ -84,7 +50,7 @@ func generateRSAKeyPair(keyID string) (*KeyPair, error) { privateKey.Precompute() // Convert to JWK format - privateJWK := JWK{ + privateJWK := config.JWK{ KeyType: "RSA", KeyID: keyID, Use: "sig", @@ -101,24 +67,10 @@ func generateRSAKeyPair(keyID string) (*KeyPair, error) { FirstCRTCoefficient: base64.RawURLEncoding.EncodeToString(privateKey.Precomputed.Qinv.Bytes()), } - publicJWK := JWK{ - KeyType: "RSA", - KeyID: keyID, - Use: "sig", - KeyOps: []string{"verify"}, - Algorithm: "RS256", - Extractable: cast.Ptr(true), - Modulus: privateJWK.Modulus, - Exponent: privateJWK.Exponent, - } - - return &KeyPair{ - PublicKey: publicJWK, - PrivateKey: privateJWK, - }, nil + return &privateJWK, nil } -func generateECDSAKeyPair(keyID string) (*KeyPair, error) { +func generateECDSAKeyPair(keyID uuid.UUID) (*config.JWK, error) { // Generate ECDSA key pair (P-256 curve for ES256) privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { @@ -128,7 +80,7 @@ func generateECDSAKeyPair(keyID string) (*KeyPair, error) { publicKey := &privateKey.PublicKey // Convert to JWK format - privateJWK := JWK{ + privateJWK := config.JWK{ KeyType: "EC", KeyID: keyID, Use: "sig", @@ -141,22 +93,7 @@ func generateECDSAKeyPair(keyID string) (*KeyPair, error) { PrivateExponent: base64.RawURLEncoding.EncodeToString(privateKey.D.Bytes()), } - publicJWK := JWK{ - KeyType: "EC", - KeyID: keyID, - Use: "sig", - KeyOps: []string{"verify"}, - Algorithm: "ES256", - Extractable: cast.Ptr(true), - Curve: "P-256", - X: privateJWK.X, - Y: privateJWK.Y, - } - - return &KeyPair{ - PublicKey: publicJWK, - PrivateKey: privateJWK, - }, nil + return &privateJWK, nil } // Run generates a key pair and writes it to the specified file path @@ -168,13 +105,13 @@ func Run(ctx context.Context, algorithm string, appendMode bool, fsys afero.Fs) outputPath := utils.Config.Auth.SigningKeysPath // Generate key pair - keyPair, err := GenerateKeyPair(Algorithm(algorithm)) + privateJWK, err := GeneratePrivateKey(config.Algorithm(algorithm)) if err != nil { return err } out := io.Writer(os.Stdout) - var jwkArray []JWK + var jwkArray []config.JWK if len(outputPath) > 0 { if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(outputPath)); err != nil { return err @@ -210,7 +147,7 @@ func Run(ctx context.Context, algorithm string, appendMode bool, fsys afero.Fs) } out = f } - jwkArray = append(jwkArray, keyPair.PrivateKey) + jwkArray = append(jwkArray, *privateJWK) // Write to file enc := json.NewEncoder(out) @@ -245,5 +182,5 @@ signing_keys_path = "./signing_key.json" // GetSupportedAlgorithms returns a list of supported algorithms func GetSupportedAlgorithms() []string { - return []string{string(AlgRS256), string(AlgES256)} + return []string{string(config.AlgRS256), string(config.AlgES256)} } diff --git a/internal/gen/signingkeys/signingkeys_test.go b/internal/gen/signingkeys/signingkeys_test.go index 51333887d..6c3a7c63d 100644 --- a/internal/gen/signingkeys/signingkeys_test.go +++ b/internal/gen/signingkeys/signingkeys_test.go @@ -2,22 +2,24 @@ package signingkeys import ( "testing" + + "github.com/supabase/cli/pkg/config" ) func TestGenerateKeyPair(t *testing.T) { tests := []struct { name string - algorithm Algorithm + algorithm config.Algorithm wantErr bool }{ { name: "RSA key generation", - algorithm: AlgRS256, + algorithm: config.AlgRS256, wantErr: false, }, { name: "ECDSA key generation", - algorithm: AlgES256, + algorithm: config.AlgES256, wantErr: false, }, { @@ -29,61 +31,62 @@ func TestGenerateKeyPair(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - keyPair, err := GenerateKeyPair(tt.algorithm) + privateJWK, err := GeneratePrivateKey(tt.algorithm) if (err != nil) != tt.wantErr { t.Errorf("GenerateKeyPair(%s) error = %v, wantErr %v", tt.algorithm, err, tt.wantErr) return } if !tt.wantErr { - if keyPair == nil { + if privateJWK == nil { t.Error("GenerateKeyPair() returned nil key pair") return } // Check that both public and private keys are generated - if keyPair.PublicKey.KeyType == "" { + publicJWK := privateJWK.ToPublicJWK() + if publicJWK.KeyType == "" { t.Error("Public key type is empty") } - if keyPair.PrivateKey.KeyType == "" { + if privateJWK.KeyType == "" { t.Error("Private key type is empty") } // Check that key IDs match - if keyPair.PublicKey.KeyID != keyPair.PrivateKey.KeyID { + if publicJWK.KeyID != privateJWK.KeyID { t.Error("Public and private key IDs don't match") } // Algorithm-specific checks switch tt.algorithm { - case AlgRS256: - if keyPair.PublicKey.KeyType != "RSA" { - t.Errorf("Expected RSA key type, got %s", keyPair.PublicKey.KeyType) + case config.AlgRS256: + if publicJWK.KeyType != "RSA" { + t.Errorf("Expected RSA key type, got %s", publicJWK.KeyType) } - if keyPair.PrivateKey.Algorithm != "RS256" { - t.Errorf("Expected RS256 algorithm, got %s", keyPair.PrivateKey.Algorithm) + if privateJWK.Algorithm != "RS256" { + t.Errorf("Expected RS256 algorithm, got %s", privateJWK.Algorithm) } // Check that RSA-specific fields are present - if keyPair.PrivateKey.Modulus == "" { + if privateJWK.Modulus == "" { t.Error("RSA private key missing modulus") } - if keyPair.PrivateKey.PrivateExponent == "" { + if privateJWK.PrivateExponent == "" { t.Error("RSA private key missing private exponent") } - case AlgES256: - if keyPair.PublicKey.KeyType != "EC" { - t.Errorf("Expected EC key type, got %s", keyPair.PublicKey.KeyType) + case config.AlgES256: + if publicJWK.KeyType != "EC" { + t.Errorf("Expected EC key type, got %s", publicJWK.KeyType) } - if keyPair.PrivateKey.Algorithm != "ES256" { - t.Errorf("Expected ES256 algorithm, got %s", keyPair.PrivateKey.Algorithm) + if privateJWK.Algorithm != "ES256" { + t.Errorf("Expected ES256 algorithm, got %s", privateJWK.Algorithm) } // Check that EC-specific fields are present - if keyPair.PrivateKey.Curve != "P-256" { - t.Errorf("Expected P-256 curve, got %s", keyPair.PrivateKey.Curve) + if privateJWK.Curve != "P-256" { + t.Errorf("Expected P-256 curve, got %s", privateJWK.Curve) } - if keyPair.PrivateKey.X == "" { + if privateJWK.X == "" { t.Error("EC private key missing X coordinate") } - if keyPair.PrivateKey.Y == "" { + if privateJWK.Y == "" { t.Error("EC private key missing Y coordinate") } } diff --git a/internal/gen/types/types.go b/internal/gen/types/types.go index 26f3c09bf..5aeeb1bd6 100644 --- a/internal/gen/types/types.go +++ b/internal/gen/types/types.go @@ -78,7 +78,7 @@ func Run(ctx context.Context, projectId string, dbConfig pgconn.Config, lang str fmt.Fprintln(os.Stderr, "Connecting to", dbConfig.Host, dbConfig.Port) escaped := utils.ToPostgresURL(dbConfig) - if require, err := isRequireSSL(ctx, originalURL, options...); err != nil { + if require, err := IsRequireSSL(ctx, originalURL, options...); err != nil { return err } else if require { // node-postgres does not support sslmode=prefer @@ -106,7 +106,7 @@ func Run(ctx context.Context, projectId string, dbConfig pgconn.Config, lang str ) } -func isRequireSSL(ctx context.Context, dbUrl string, options ...func(*pgx.ConnConfig)) (bool, error) { +func IsRequireSSL(ctx context.Context, dbUrl string, options ...func(*pgx.ConnConfig)) (bool, error) { conn, err := utils.ConnectByUrl(ctx, dbUrl+"&sslmode=require", options...) if err != nil { if strings.HasSuffix(err.Error(), "(server refused TLS connection)") { diff --git a/internal/hostnames/activate/activate.go b/internal/hostnames/activate/activate.go index 3cc01c206..c2306a29e 100644 --- a/internal/hostnames/activate/activate.go +++ b/internal/hostnames/activate/activate.go @@ -27,7 +27,7 @@ func Run(ctx context.Context, projectRef string, includeRawOutput bool, fsys afe { resp, err := utils.GetSupabase().V1ActivateCustomHostnameWithResponse(ctx, projectRef) if err != nil { - return errors.Errorf("failed to active custom hostname: %w", err) + return errors.Errorf("failed to activate custom hostname: %w", err) } if resp.JSON201 == nil { return errors.New("failed to activate custom hostname config: " + string(resp.Body)) diff --git a/internal/hostnames/common_test.go b/internal/hostnames/common_test.go index c085afa53..e46e202ab 100644 --- a/internal/hostnames/common_test.go +++ b/internal/hostnames/common_test.go @@ -7,9 +7,11 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/utils" ) func TestVerifyCNAME(t *testing.T) { + utils.CurrentProfile.ProjectHost = "supabase.co" defer gock.OffAll() gock.New("https://1.1.1.1"). Get("/dns-query"). @@ -40,5 +42,5 @@ func TestVerifyCNAMEFailures(t *testing.T) { }, }}) err := VerifyCNAME(context.Background(), "foobarbaz", "hello.custom-domain.com") - assert.ErrorContains(t, err, "expected custom hostname 'hello.custom-domain.com' to have a CNAME record pointing to your project at 'foobarbaz.supabase.co.', but it failed to resolve: failed to locate appropriate CNAME record for hello.custom-domain.com") + assert.ErrorContains(t, err, "failed to locate appropriate CNAME record for hello.custom-domain.com") } diff --git a/internal/inspect/calls/calls.sql b/internal/inspect/calls/calls.sql index 57c3ecee2..51d404ba6 100644 --- a/internal/inspect/calls/calls.sql +++ b/internal/inspect/calls/calls.sql @@ -3,7 +3,24 @@ SELECT (interval '1 millisecond' * total_exec_time)::text AS total_exec_time, to_char((total_exec_time/sum(total_exec_time) OVER()) * 100, 'FM90D0') || '%' AS prop_exec_time, to_char(calls, 'FM999G999G990') AS ncalls, - (interval '1 millisecond' * (blk_read_time + blk_write_time))::text AS sync_io_time -FROM pg_stat_statements + /* + Handle column names for 15 and 17 + */ + ( + interval '1 millisecond' * ( + COALESCE( + (to_jsonb(s) ->> 'shared_blk_read_time')::double precision, + (to_jsonb(s) ->> 'blk_read_time')::double precision, + 0 + ) + + + COALESCE( + (to_jsonb(s) ->> 'shared_blk_write_time')::double precision, + (to_jsonb(s) ->> 'blk_write_time')::double precision, + 0 + ) + ) + )::text AS sync_io_time +FROM pg_stat_statements s ORDER BY calls DESC LIMIT 10 diff --git a/internal/inspect/db_stats/db_stats.sql b/internal/inspect/db_stats/db_stats.sql index d7fc02f98..6795c330b 100644 --- a/internal/inspect/db_stats/db_stats.sql +++ b/internal/inspect/db_stats/db_stats.sql @@ -12,16 +12,47 @@ WITH total_objects AS ( UNION SELECT 't' AS relkind, - ROUND(SUM(heap_blks_hit)::numeric / nullif(SUM(heap_blks_hit + heap_blks_read), 0), 2) AS ratio - FROM pg_statio_user_tables + /* + Handle column names for both PG15 and 17 + */ + ROUND( + ( + SUM( + COALESCE( + (to_jsonb(s) ->> 'rel_blks_hit')::bigint, + (to_jsonb(s) ->> 'heap_blks_hit')::bigint, + 0 + ) + )::numeric + / + nullif( + SUM( + COALESCE( + (to_jsonb(s) ->> 'rel_blks_hit')::bigint, + (to_jsonb(s) ->> 'heap_blks_hit')::bigint, + 0 + ) + + + COALESCE( + (to_jsonb(s) ->> 'rel_blks_read')::bigint, + (to_jsonb(s) ->> 'heap_blks_read')::bigint, + 0 + ) + ), + 0 + ) + ), + 2 + ) AS ratio + FROM pg_statio_user_tables s WHERE NOT schemaname LIKE ANY($1) ) SELECT pg_size_pretty(pg_database_size($2)) AS database_size, - (SELECT size FROM total_objects WHERE relkind = 'i') AS total_index_size, - (SELECT size FROM total_objects WHERE relkind = 'r') AS total_table_size, - (SELECT size FROM total_objects WHERE relkind = 't') AS total_toast_size, - (SELECT (now() - stats_reset)::text FROM pg_stat_statements_info) AS time_since_stats_reset, + COALESCE((SELECT size FROM total_objects WHERE relkind = 'i'), '0 bytes') AS total_index_size, + COALESCE((SELECT size FROM total_objects WHERE relkind = 'r'), '0 bytes') AS total_table_size, + COALESCE((SELECT size FROM total_objects WHERE relkind = 't'), '0 bytes') AS total_toast_size, + COALESCE((SELECT (now() - stats_reset)::text FROM pg_stat_statements_info), 'N/A') AS time_since_stats_reset, (SELECT COALESCE(ratio::text, 'N/A') FROM cache_hit WHERE relkind = 'i') AS index_hit_rate, (SELECT COALESCE(ratio::text, 'N/A') FROM cache_hit WHERE relkind = 't') AS table_hit_rate, - (SELECT pg_size_pretty(SUM(size)) FROM pg_ls_waldir()) AS wal_size + COALESCE((SELECT pg_size_pretty(SUM(size)) FROM pg_ls_waldir()), '0 bytes') AS wal_size diff --git a/internal/inspect/outliers/outliers.sql b/internal/inspect/outliers/outliers.sql index d222cc8de..894e65c7e 100644 --- a/internal/inspect/outliers/outliers.sql +++ b/internal/inspect/outliers/outliers.sql @@ -2,8 +2,25 @@ SELECT (interval '1 millisecond' * total_exec_time)::text AS total_exec_time, to_char((total_exec_time/sum(total_exec_time) OVER()) * 100, 'FM90D0') || '%' AS prop_exec_time, to_char(calls, 'FM999G999G999G990') AS ncalls, - (interval '1 millisecond' * (blk_read_time + blk_write_time))::text AS sync_io_time, + /* + Handle column names for 15 and 17 + */ + ( + interval '1 millisecond' * ( + COALESCE( + (to_jsonb(s) ->> 'shared_blk_read_time')::double precision, + (to_jsonb(s) ->> 'blk_read_time')::double precision, + 0 + ) + + + COALESCE( + (to_jsonb(s) ->> 'shared_blk_write_time')::double precision, + (to_jsonb(s) ->> 'blk_write_time')::double precision, + 0 + ) + ) + )::text AS sync_io_time, query -FROM pg_stat_statements WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1) +FROM pg_stat_statements s WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1) ORDER BY total_exec_time DESC LIMIT 10 diff --git a/internal/link/link.go b/internal/link/link.go index 9bf77675e..cffdf1339 100644 --- a/internal/link/link.go +++ b/internal/link/link.go @@ -19,17 +19,11 @@ import ( "github.com/supabase/cli/pkg/api" "github.com/supabase/cli/pkg/cast" cliConfig "github.com/supabase/cli/pkg/config" - "github.com/supabase/cli/pkg/diff" "github.com/supabase/cli/pkg/migration" ) func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { - copy := utils.Config.Clone() - original, err := cliConfig.ToTomlBytes(copy) - if err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - } - + majorVersion := utils.Config.Db.MajorVersion if err := checkRemoteProjectStatus(ctx, projectRef, fsys); err != nil { return err } @@ -54,14 +48,12 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func( fmt.Fprintln(os.Stdout, "Finished "+utils.Aqua("supabase link")+".") // 4. Suggest config update - updated, err := cliConfig.ToTomlBytes(utils.Config.Clone()) - if err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - } - - if lineDiff := diff.Diff(utils.ConfigPath, original, projectRef, updated); len(lineDiff) > 0 { - fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "Local config differs from linked project. Try updating", utils.Bold(utils.ConfigPath)) - fmt.Println(string(lineDiff)) + if utils.Config.Db.MajorVersion != majorVersion { + fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "Local database version differs from the linked project.") + fmt.Fprintf(os.Stderr, `Update your %s to fix it: +[db] +major_version = %d +`, utils.Bold(utils.ConfigPath), utils.Config.Db.MajorVersion) } return nil } diff --git a/internal/login/login_test.go b/internal/login/login_test.go index 758fbc5c4..ef6772351 100644 --- a/internal/login/login_test.go +++ b/internal/login/login_test.go @@ -40,7 +40,7 @@ func TestLoginCommand(t *testing.T) { Token: token, Fsys: afero.NewMemMapFs(), })) - saved, err := credentials.StoreProvider.Get(utils.AccessTokenKey) + saved, err := credentials.StoreProvider.Get(utils.CurrentProfile.Name) assert.NoError(t, err) assert.Equal(t, token, saved) }) @@ -83,7 +83,7 @@ func TestLoginCommand(t *testing.T) { expectedBrowserUrl := fmt.Sprintf("%s/cli/login?session_id=%s&token_name=%s&public_key=%s", utils.GetSupabaseDashboardURL(), sessionId, tokenName, publicKey) assert.Contains(t, out.String(), expectedBrowserUrl) - saved, err := credentials.StoreProvider.Get(utils.AccessTokenKey) + saved, err := credentials.StoreProvider.Get(utils.CurrentProfile.Name) assert.NoError(t, err) assert.Equal(t, token, saved) assert.Empty(t, apitest.ListUnmatchedRequests()) diff --git a/internal/logout/logout_test.go b/internal/logout/logout_test.go index 943f25bed..883ebd8bf 100644 --- a/internal/logout/logout_test.go +++ b/internal/logout/logout_test.go @@ -35,7 +35,7 @@ func TestLogoutCommand(t *testing.T) { t.Run("removes all Supabase CLI credentials", func(t *testing.T) { keyring.MockInit() - require.NoError(t, credentials.StoreProvider.Set(utils.AccessTokenKey, token)) + require.NoError(t, credentials.StoreProvider.Set(utils.CurrentProfile.Name, token)) require.NoError(t, credentials.StoreProvider.Set("project1", "password1")) require.NoError(t, credentials.StoreProvider.Set("project2", "password2")) t.Cleanup(fstest.MockStdin(t, "y")) @@ -44,7 +44,7 @@ func TestLogoutCommand(t *testing.T) { // Check error assert.NoError(t, err) // Check that access token has been removed - saved, _ := credentials.StoreProvider.Get(utils.AccessTokenKey) + saved, _ := credentials.StoreProvider.Get(utils.CurrentProfile.Name) assert.Empty(t, saved) // check that project 1 has been removed saved, _ = credentials.StoreProvider.Get("project1") @@ -56,14 +56,14 @@ func TestLogoutCommand(t *testing.T) { t.Run("skips logout by default", func(t *testing.T) { keyring.MockInit() - require.NoError(t, credentials.StoreProvider.Set(utils.AccessTokenKey, token)) + require.NoError(t, credentials.StoreProvider.Set(utils.CurrentProfile.Name, token)) // Setup in-memory fs fsys := afero.NewMemMapFs() // Run test err := Run(context.Background(), os.Stdout, fsys) // Check error assert.ErrorIs(t, err, context.Canceled) - saved, err := credentials.StoreProvider.Get(utils.AccessTokenKey) + saved, err := credentials.StoreProvider.Get(utils.CurrentProfile.Name) assert.NoError(t, err) assert.Equal(t, token, saved) }) diff --git a/internal/start/start.go b/internal/start/start.go index 7ba53d0e7..698be1c0f 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -51,16 +51,14 @@ func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignore } } - if err := utils.RunProgram(ctx, func(p utils.Program, ctx context.Context) error { - dbConfig := pgconn.Config{ - Host: utils.DbId, - Port: 5432, - User: "postgres", - Password: utils.Config.Db.Password, - Database: "postgres", - } - return run(p, ctx, fsys, excludedContainers, dbConfig) - }); err != nil { + dbConfig := pgconn.Config{ + Host: utils.DbId, + Port: 5432, + User: "postgres", + Password: utils.Config.Db.Password, + Database: "postgres", + } + if err := run(ctx, fsys, excludedContainers, dbConfig); err != nil { if ignoreHealthCheck && start.IsUnhealthyError(err) { fmt.Fprintln(os.Stderr, err) } else { @@ -139,7 +137,7 @@ var ( var serviceTimeout = 30 * time.Second -func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers []string, dbConfig pgconn.Config, options ...func(*pgx.ConnConfig)) error { +func run(ctx context.Context, fsys afero.Fs, excludedContainers []string, dbConfig pgconn.Config, options ...func(*pgx.ConnConfig)) error { excluded := make(map[string]bool) for _, name := range excludedContainers { excluded[name] = true @@ -151,9 +149,8 @@ func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers } // Start Postgres. - w := utils.StatusWriter{Program: p} if dbConfig.Host == utils.DbId { - if err := start.StartDatabase(ctx, "", fsys, w, options...); err != nil { + if err := start.StartDatabase(ctx, "", fsys, os.Stderr, options...); err != nil { return err } } @@ -162,7 +159,7 @@ func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers var isStorageEnabled = utils.Config.Storage.Enabled && !isContainerExcluded(utils.Config.Storage.Image, excluded) var isImgProxyEnabled = utils.Config.Storage.ImageTransformation != nil && utils.Config.Storage.ImageTransformation.Enabled && !isContainerExcluded(utils.Config.Storage.ImgProxyImage, excluded) - p.Send(utils.StatusMsg("Starting containers...")) + fmt.Fprintln(os.Stderr, "Starting containers...") // Start Logflare if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.Config.Analytics.Image, excluded) { @@ -1118,7 +1115,7 @@ EOF started = append(started, utils.PoolerId) } - p.Send(utils.StatusMsg("Waiting for health checks...")) + fmt.Fprintln(os.Stderr, "Waiting for health checks...") if utils.NoBackupVolume && utils.SliceContains(started, utils.StorageId) { if err := start.WaitForHealthyService(ctx, serviceTimeout, utils.StorageId); err != nil { return err diff --git a/internal/start/start_test.go b/internal/start/start_test.go index d52bc4590..99df75378 100644 --- a/internal/start/start_test.go +++ b/internal/start/start_test.go @@ -200,9 +200,7 @@ func TestDatabaseStart(t *testing.T) { Reply(http.StatusOK). JSON([]storage.BucketResponse{}) // Run test - err := utils.RunProgram(context.Background(), func(p utils.Program, ctx context.Context) error { - return run(p, context.Background(), fsys, []string{}, pgconn.Config{Host: utils.DbId}, conn.Intercept) - }) + err := run(context.Background(), fsys, []string{}, pgconn.Config{Host: utils.DbId}, conn.Intercept) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -246,9 +244,7 @@ func TestDatabaseStart(t *testing.T) { // Run test exclude := ExcludableContainers() exclude = append(exclude, "invalid", exclude[0]) - err := utils.RunProgram(context.Background(), func(p utils.Program, ctx context.Context) error { - return run(p, context.Background(), fsys, exclude, pgconn.Config{Host: utils.DbId}) - }) + err := run(context.Background(), fsys, exclude, pgconn.Config{Host: utils.DbId}) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) diff --git a/internal/utils/access_token.go b/internal/utils/access_token.go index 26b3dfddb..6d1c06b70 100644 --- a/internal/utils/access_token.go +++ b/internal/utils/access_token.go @@ -1,6 +1,7 @@ package utils import ( + "fmt" "os" "path/filepath" "regexp" @@ -32,12 +33,20 @@ func LoadAccessTokenFS(fsys afero.Fs) (string, error) { } func loadAccessToken(fsys afero.Fs) (string, error) { + logger := GetDebugLogger() // Env takes precedence if accessToken := os.Getenv("SUPABASE_ACCESS_TOKEN"); accessToken != "" { + fmt.Fprintln(logger, "Using access token from env var...") return accessToken, nil } - // Load from native credentials store + // Load from current profile + if accessToken, err := credentials.StoreProvider.Get(CurrentProfile.Name); err == nil { + fmt.Fprintln(logger, "Using access token for profile:", CurrentProfile.Name) + return accessToken, nil + } + // Load from legacy key for backwards compatibility if accessToken, err := credentials.StoreProvider.Get(AccessTokenKey); err == nil { + fmt.Fprintln(logger, "Using access token from credentials store...") return accessToken, nil } // Fallback to token file @@ -55,6 +64,7 @@ func fallbackLoadToken(fsys afero.Fs) (string, error) { } else if err != nil { return "", errors.Errorf("failed to read access token file: %w", err) } + fmt.Fprintln(GetDebugLogger(), "Using access token from file:", path) return string(accessToken), nil } @@ -63,8 +73,10 @@ func SaveAccessToken(accessToken string, fsys afero.Fs) error { if !AccessTokenPattern.MatchString(accessToken) { return errors.New(ErrInvalidToken) } - // Save to native credentials store - if err := credentials.StoreProvider.Set(AccessTokenKey, accessToken); err == nil { + // Save to current profile + if err := credentials.StoreProvider.Set(CurrentProfile.Name, accessToken); err != nil { + fmt.Fprintln(GetDebugLogger(), err) + } else { return nil } // Fallback to token file @@ -87,19 +99,20 @@ func fallbackSaveToken(accessToken string, fsys afero.Fs) error { func DeleteAccessToken(fsys afero.Fs) error { // Always delete the fallback token file to handle legacy CLI - if err := fallbackDeleteToken(fsys); err == nil { - // Typically user system should only have either token file or keyring. - // But we delete from both just in case. - _ = credentials.StoreProvider.Delete(AccessTokenKey) - return nil - } else if !errors.Is(err, os.ErrNotExist) { + if err := fallbackDeleteToken(fsys); err != nil && !errors.Is(err, os.ErrNotExist) { return err } - // Fallback not found, delete from native credentials store - err := credentials.StoreProvider.Delete(AccessTokenKey) - if errors.Is(err, credentials.ErrNotSupported) || errors.Is(err, keyring.ErrNotFound) { - return errors.New(ErrNotLoggedIn) - } else if err != nil { + // Always delete from legacy keyring + if err := credentials.StoreProvider.Delete(AccessTokenKey); err != nil && !errors.Is(err, keyring.ErrNotFound) { + fmt.Fprintln(GetDebugLogger(), err) + } + // Delete profile from native credentials store + if err := credentials.StoreProvider.Delete(CurrentProfile.Name); err != nil { + if errors.Is(err, credentials.ErrNotSupported) || + errors.Is(err, keyring.ErrUnsupportedPlatform) || + errors.Is(err, keyring.ErrNotFound) { + return errors.New(ErrNotLoggedIn) + } return errors.Errorf("failed to delete access token from keyring: %w", err) } return nil diff --git a/internal/utils/access_token_test.go b/internal/utils/access_token_test.go index f31988876..77743d30d 100644 --- a/internal/utils/access_token_test.go +++ b/internal/utils/access_token_test.go @@ -166,6 +166,7 @@ func TestDeleteToken(t *testing.T) { t.Run("deletes both keyring and fallback", func(t *testing.T) { token := string(apitest.RandomAccessToken(t)) require.NoError(t, credentials.StoreProvider.Set(AccessTokenKey, token)) + require.NoError(t, credentials.StoreProvider.Set(CurrentProfile.Name, token)) // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, fallbackSaveToken(token, fsys)) @@ -173,6 +174,8 @@ func TestDeleteToken(t *testing.T) { err := DeleteAccessToken(fsys) // Check error assert.NoError(t, err) + _, err = credentials.StoreProvider.Get(CurrentProfile.Name) + assert.ErrorIs(t, err, keyring.ErrNotFound) _, err = credentials.StoreProvider.Get(AccessTokenKey) assert.ErrorIs(t, err, keyring.ErrNotFound) path, err := getAccessTokenPath() diff --git a/internal/utils/api.go b/internal/utils/api.go index c67252b6b..3cc1dd380 100644 --- a/internal/utils/api.go +++ b/internal/utils/api.go @@ -11,7 +11,6 @@ import ( "github.com/go-errors/errors" "github.com/spf13/afero" - "github.com/spf13/viper" "github.com/supabase/cli/internal/utils/cloudflare" supabase "github.com/supabase/cli/pkg/api" "github.com/supabase/cli/pkg/cast" @@ -139,11 +138,8 @@ func GetSupabase() *supabase.ClientWithResponses { return apiClient } -const ( - DefaultApiHost = "https://api.supabase.com" - // DEPRECATED - DeprecatedApiHost = "https://api.supabase.io" -) +// Used by unit tests +var DefaultApiHost = CurrentProfile.APIURL var RegionMap = map[string]string{ "ap-northeast-1": "Northeast Asia (Tokyo)", @@ -163,43 +159,17 @@ var RegionMap = map[string]string{ } func GetSupabaseAPIHost() string { - apiHost := viper.GetString("INTERNAL_API_HOST") - if apiHost == "" { - apiHost = DefaultApiHost - } - return apiHost + return CurrentProfile.APIURL } func GetSupabaseDashboardURL() string { - switch GetSupabaseAPIHost() { - case DefaultApiHost, DeprecatedApiHost: - return "https://supabase.com/dashboard" - case "https://api.supabase.green": - return "https://supabase.green/dashboard" - default: - return "http://127.0.0.1:8082" - } + return CurrentProfile.DashboardURL } -func GetSupabaseDbHost(projectRef string) string { - // TODO: query projects api for db_host - switch GetSupabaseAPIHost() { - case DefaultApiHost, DeprecatedApiHost: - return fmt.Sprintf("db.%s.supabase.co", projectRef) - case "https://api.supabase.green": - return fmt.Sprintf("db.%s.supabase.red", projectRef) - default: - return fmt.Sprintf("db.%s.supabase.red", projectRef) - } +func GetSupabaseHost(projectRef string) string { + return fmt.Sprintf("%s.%s", projectRef, CurrentProfile.ProjectHost) } -func GetSupabaseHost(projectRef string) string { - switch GetSupabaseAPIHost() { - case DefaultApiHost, DeprecatedApiHost: - return fmt.Sprintf("%s.supabase.co", projectRef) - case "https://api.supabase.green": - return fmt.Sprintf("%s.supabase.red", projectRef) - default: - return fmt.Sprintf("%s.supabase.red", projectRef) - } +func GetSupabaseDbHost(projectRef string) string { + return "db." + GetSupabaseHost(projectRef) } diff --git a/internal/utils/flags/db_url.go b/internal/utils/flags/db_url.go index 027e3ec3a..5caedda32 100644 --- a/internal/utils/flags/db_url.go +++ b/internal/utils/flags/db_url.go @@ -138,6 +138,8 @@ func NewDbConfigWithPassword(ctx context.Context, projectRef string) pgconn.Conf } config.User = newRole.User return config + } else { + fmt.Fprintln(utils.GetDebugLogger(), err) } } if config.Password, err = credentials.StoreProvider.Get(projectRef); err == nil { diff --git a/internal/utils/flags/project_ref.go b/internal/utils/flags/project_ref.go index c1629c0fa..3be14838e 100644 --- a/internal/utils/flags/project_ref.go +++ b/internal/utils/flags/project_ref.go @@ -52,16 +52,20 @@ func PromptProjectRef(ctx context.Context, title string, opts ...tea.ProgramOpti } func LoadProjectRef(fsys afero.Fs) error { + debuglogger := utils.GetDebugLogger() // Flag takes highest precedence if len(ProjectRef) > 0 { + fmt.Fprintf(debuglogger, "Loading project ref from flag: %s\n", ProjectRef) return utils.AssertProjectRefIsValid(ProjectRef) } // Env var takes precedence over ref file if ProjectRef = viper.GetString("PROJECT_ID"); len(ProjectRef) > 0 { + fmt.Fprintf(debuglogger, "Loading project ref from env var: %s\n", ProjectRef) return utils.AssertProjectRefIsValid(ProjectRef) } // Load from local file last projectRefBytes, err := afero.ReadFile(fsys, utils.ProjectRefPath) + fmt.Fprintf(debuglogger, "Loading project ref from file: %s\n", utils.ProjectRefPath) if errors.Is(err, os.ErrNotExist) { return errors.New(utils.ErrNotLinked) } else if err != nil { diff --git a/internal/utils/profile.go b/internal/utils/profile.go new file mode 100644 index 000000000..55afa4e89 --- /dev/null +++ b/internal/utils/profile.go @@ -0,0 +1,68 @@ +package utils + +import ( + "context" + "strings" + + "github.com/go-errors/errors" + "github.com/go-playground/validator/v10" + "github.com/spf13/afero" + "github.com/spf13/viper" +) + +type Profile struct { + Name string `mapstructure:"name" validate:"required"` + APIURL string `mapstructure:"api_url" validate:"required,http_url"` + DashboardURL string `mapstructure:"dashboard_url" validate:"required,http_url"` + ProjectHost string `mapstructure:"project_host" validate:"required,hostname_rfc1123"` + DocsURL string `mapstructure:"docs_url" validate:"omitempty,http_url"` + StudioImage string `mapstructure:"studio_image"` +} + +var allProfiles = []Profile{{ + Name: "supabase", + APIURL: "https://api.supabase.com", + DashboardURL: "https://supabase.com/dashboard", + DocsURL: "https://supabase.com/docs", + ProjectHost: "supabase.co", +}, { + Name: "supabase-staging", + APIURL: "https://api.supabase.green", + DashboardURL: "https://supabase.green/dashboard", + DocsURL: "https://supabase.com/docs", + ProjectHost: "supabase.red", +}, { + Name: "supabase-local", + APIURL: "http://localhost:8080", + DashboardURL: "http://localhost:8082", + DocsURL: "https://supabase.com/docs", + ProjectHost: "supabase.red", +}} + +var CurrentProfile Profile + +func LoadProfile(ctx context.Context, fsys afero.Fs) error { + prof := viper.GetString("PROFILE") + for _, p := range allProfiles { + if strings.EqualFold(p.Name, prof) { + CurrentProfile = p + return nil + } + } + // Instantiate to avoid leaking profile into global viper state + v := viper.New() + v.SetFs(fsys) + v.SetConfigFile(prof) + if err := v.ReadInConfig(); err != nil { + return errors.Errorf("failed to read profile: %w", err) + } + // Load profile into viper, rejecting keys not defined by config + if err := v.UnmarshalExact(&CurrentProfile); err != nil { + return errors.Errorf("failed to parse profile: %w", err) + } + validate := validator.New(validator.WithRequiredStructEnabled()) + if err := validate.StructCtx(ctx, &CurrentProfile); err != nil { + return errors.Errorf("invalid profile: %w", err) + } + return nil +} diff --git a/internal/utils/profile_test.go b/internal/utils/profile_test.go new file mode 100644 index 000000000..c3ebcf39d --- /dev/null +++ b/internal/utils/profile_test.go @@ -0,0 +1,78 @@ +package utils + +import ( + "context" + "embed" + "os" + "testing" + + "github.com/go-playground/validator/v10" + "github.com/spf13/afero" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +//go:embed testdata/*.json +var testdata embed.FS + +func TestLoadProfile(t *testing.T) { + validate := validator.New(validator.WithRequiredStructEnabled()) + for _, p := range allProfiles { + t.Run("loads profile "+p.Name, func(t *testing.T) { + viper.Set("PROFILE", p.Name) + t.Cleanup(viper.Reset) + // Setup in-memory fs + fsys := afero.NewMemMapFs() + // Run test + err := LoadProfile(context.Background(), fsys) + // Check error + assert.NoError(t, err) + assert.NoError(t, validate.Struct(&CurrentProfile)) + }) + } + + t.Run("loads from json", func(t *testing.T) { + viper.Set("PROFILE", "testdata/profile.json") + t.Cleanup(viper.Reset) + // Setup in-memory fs + fsys := afero.FromIOFS{FS: testdata} + // Run test + err := LoadProfile(context.Background(), fsys) + // Check error + assert.NoError(t, err) + }) + + t.Run("throws error on invalid profile", func(t *testing.T) { + viper.Set("PROFILE", "testdata/invalid.json") + t.Cleanup(viper.Reset) + // Setup in-memory fs + fsys := afero.FromIOFS{FS: testdata} + // Run test + err := LoadProfile(context.Background(), fsys) + // Check error + assert.ErrorContains(t, err, "Field validation for 'APIURL' failed on the 'http_url' tag") + assert.ErrorContains(t, err, "Field validation for 'ProjectHost' failed on the 'hostname_rfc1123' tag") + }) + + t.Run("throws error on malformed profile", func(t *testing.T) { + viper.Set("PROFILE", "testdata/malformed.json") + t.Cleanup(viper.Reset) + // Setup in-memory fs + fsys := afero.FromIOFS{FS: testdata} + // Run test + err := LoadProfile(context.Background(), fsys) + // Check error + assert.ErrorContains(t, err, "invalid keys: test_url") + }) + + t.Run("throws error on missing profile", func(t *testing.T) { + viper.Set("PROFILE", "testdata/missing.json") + t.Cleanup(viper.Reset) + // Setup in-memory fs + fsys := afero.FromIOFS{FS: testdata} + // Run test + err := LoadProfile(context.Background(), fsys) + // Check error + assert.ErrorIs(t, err, os.ErrNotExist) + }) +} diff --git a/internal/utils/testdata/invalid.json b/internal/utils/testdata/invalid.json new file mode 100644 index 000000000..4182e5695 --- /dev/null +++ b/internal/utils/testdata/invalid.json @@ -0,0 +1,6 @@ +{ + "name": "supabase", + "api_url": "api.supabase.com", + "dashboard_url": "https://supabase.com/dashboard", + "project_host": "https://supabase.co" +} diff --git a/internal/utils/testdata/malformed.json b/internal/utils/testdata/malformed.json new file mode 100644 index 000000000..55a4e3c2a --- /dev/null +++ b/internal/utils/testdata/malformed.json @@ -0,0 +1,4 @@ +{ + "name": "supabase", + "test_url": "https://api.supabase.com" +} diff --git a/internal/utils/testdata/profile.json b/internal/utils/testdata/profile.json new file mode 100644 index 000000000..66ba4ae45 --- /dev/null +++ b/internal/utils/testdata/profile.json @@ -0,0 +1,6 @@ +{ + "name": "supabase", + "api_url": "https://api.supabase.com", + "dashboard_url": "https://supabase.com/dashboard", + "project_host": "supabase.co" +} diff --git a/pkg/api/client.gen.go b/pkg/api/client.gen.go index eae5082f6..1ca272ffd 100644 --- a/pkg/api/client.gen.go +++ b/pkg/api/client.gen.go @@ -218,7 +218,9 @@ type ClientInterface interface { V1ApplyProjectAddon(ctx context.Context, ref string, body V1ApplyProjectAddonJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) // V1RemoveProjectAddon request - V1RemoveProjectAddon(ctx context.Context, ref string, addonVariant interface{}, reqEditors ...RequestEditorFn) (*http.Response, error) + V1RemoveProjectAddon(ctx context.Context, ref string, addonVariant struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*http.Response, error) // V1DisablePreviewBranching request V1DisablePreviewBranching(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1121,7 +1123,9 @@ func (c *Client) V1ApplyProjectAddon(ctx context.Context, ref string, body V1App return c.Client.Do(req) } -func (c *Client) V1RemoveProjectAddon(ctx context.Context, ref string, addonVariant interface{}, reqEditors ...RequestEditorFn) (*http.Response, error) { +func (c *Client) V1RemoveProjectAddon(ctx context.Context, ref string, addonVariant struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewV1RemoveProjectAddonRequest(c.Server, ref, addonVariant) if err != nil { return nil, err @@ -4522,7 +4526,9 @@ func NewV1ApplyProjectAddonRequestWithBody(server string, ref string, contentTyp } // NewV1RemoveProjectAddonRequest generates requests for V1RemoveProjectAddon -func NewV1RemoveProjectAddonRequest(server string, ref string, addonVariant interface{}) (*http.Request, error) { +func NewV1RemoveProjectAddonRequest(server string, ref string, addonVariant struct { + union json.RawMessage +}) (*http.Request, error) { var err error var pathParam0 string @@ -8941,7 +8947,9 @@ type ClientWithResponsesInterface interface { V1ApplyProjectAddonWithResponse(ctx context.Context, ref string, body V1ApplyProjectAddonJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ApplyProjectAddonResponse, error) // V1RemoveProjectAddonWithResponse request - V1RemoveProjectAddonWithResponse(ctx context.Context, ref string, addonVariant interface{}, reqEditors ...RequestEditorFn) (*V1RemoveProjectAddonResponse, error) + V1RemoveProjectAddonWithResponse(ctx context.Context, ref string, addonVariant struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*V1RemoveProjectAddonResponse, error) // V1DisablePreviewBranchingWithResponse request V1DisablePreviewBranchingWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1DisablePreviewBranchingResponse, error) @@ -12527,7 +12535,9 @@ func (c *ClientWithResponses) V1ApplyProjectAddonWithResponse(ctx context.Contex } // V1RemoveProjectAddonWithResponse request returning *V1RemoveProjectAddonResponse -func (c *ClientWithResponses) V1RemoveProjectAddonWithResponse(ctx context.Context, ref string, addonVariant interface{}, reqEditors ...RequestEditorFn) (*V1RemoveProjectAddonResponse, error) { +func (c *ClientWithResponses) V1RemoveProjectAddonWithResponse(ctx context.Context, ref string, addonVariant struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*V1RemoveProjectAddonResponse, error) { rsp, err := c.V1RemoveProjectAddon(ctx, ref, addonVariant, reqEditors...) if err != nil { return nil, err diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index eb639a8fa..105856d48 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -410,41 +410,41 @@ const ( // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId0. const ( - Ci12xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_12xlarge" - Ci16xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_16xlarge" - Ci24xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_24xlarge" - Ci24xlargeHighMemory ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_24xlarge_high_memory" - Ci24xlargeOptimizedCpu ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_24xlarge_optimized_cpu" - Ci24xlargeOptimizedMemory ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_24xlarge_optimized_memory" - Ci2xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_2xlarge" - Ci48xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_48xlarge" - Ci48xlargeHighMemory ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_48xlarge_high_memory" - Ci48xlargeOptimizedCpu ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_48xlarge_optimized_cpu" - Ci48xlargeOptimizedMemory ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_48xlarge_optimized_memory" - Ci4xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_4xlarge" - Ci8xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_8xlarge" - CiLarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_large" - CiMedium ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_medium" - CiMicro ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_micro" - CiSmall ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_small" - CiXlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_xlarge" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci12xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_12xlarge" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci16xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_16xlarge" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci24xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_24xlarge" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci24xlargeHighMemory ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_24xlarge_high_memory" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci24xlargeOptimizedCpu ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_24xlarge_optimized_cpu" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci24xlargeOptimizedMemory ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_24xlarge_optimized_memory" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci2xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_2xlarge" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci48xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_48xlarge" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci48xlargeHighMemory ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_48xlarge_high_memory" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci48xlargeOptimizedCpu ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_48xlarge_optimized_cpu" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci48xlargeOptimizedMemory ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_48xlarge_optimized_memory" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci4xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_4xlarge" + ListProjectAddonsResponseSelectedAddonsVariantId0Ci8xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_8xlarge" + ListProjectAddonsResponseSelectedAddonsVariantId0CiLarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_large" + ListProjectAddonsResponseSelectedAddonsVariantId0CiMedium ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_medium" + ListProjectAddonsResponseSelectedAddonsVariantId0CiMicro ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_micro" + ListProjectAddonsResponseSelectedAddonsVariantId0CiSmall ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_small" + ListProjectAddonsResponseSelectedAddonsVariantId0CiXlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_xlarge" ) // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId1. const ( - CdDefault ListProjectAddonsResponseSelectedAddonsVariantId1 = "cd_default" + ListProjectAddonsResponseSelectedAddonsVariantId1CdDefault ListProjectAddonsResponseSelectedAddonsVariantId1 = "cd_default" ) // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId2. const ( - Pitr14 ListProjectAddonsResponseSelectedAddonsVariantId2 = "pitr_14" - Pitr28 ListProjectAddonsResponseSelectedAddonsVariantId2 = "pitr_28" - Pitr7 ListProjectAddonsResponseSelectedAddonsVariantId2 = "pitr_7" + ListProjectAddonsResponseSelectedAddonsVariantId2Pitr14 ListProjectAddonsResponseSelectedAddonsVariantId2 = "pitr_14" + ListProjectAddonsResponseSelectedAddonsVariantId2Pitr28 ListProjectAddonsResponseSelectedAddonsVariantId2 = "pitr_28" + ListProjectAddonsResponseSelectedAddonsVariantId2Pitr7 ListProjectAddonsResponseSelectedAddonsVariantId2 = "pitr_7" ) // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId3. const ( - Ipv4Default ListProjectAddonsResponseSelectedAddonsVariantId3 = "ipv4_default" + ListProjectAddonsResponseSelectedAddonsVariantId3Ipv4Default ListProjectAddonsResponseSelectedAddonsVariantId3 = "ipv4_default" ) // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId4. @@ -937,12 +937,14 @@ const ( // Defines values for V1ServiceHealthResponseName. const ( - V1ServiceHealthResponseNameAuth V1ServiceHealthResponseName = "auth" - V1ServiceHealthResponseNameDb V1ServiceHealthResponseName = "db" - V1ServiceHealthResponseNamePooler V1ServiceHealthResponseName = "pooler" - V1ServiceHealthResponseNameRealtime V1ServiceHealthResponseName = "realtime" - V1ServiceHealthResponseNameRest V1ServiceHealthResponseName = "rest" - V1ServiceHealthResponseNameStorage V1ServiceHealthResponseName = "storage" + V1ServiceHealthResponseNameAuth V1ServiceHealthResponseName = "auth" + V1ServiceHealthResponseNameDb V1ServiceHealthResponseName = "db" + V1ServiceHealthResponseNameDbPostgresUser V1ServiceHealthResponseName = "db_postgres_user" + V1ServiceHealthResponseNamePgBouncer V1ServiceHealthResponseName = "pg_bouncer" + V1ServiceHealthResponseNamePooler V1ServiceHealthResponseName = "pooler" + V1ServiceHealthResponseNameRealtime V1ServiceHealthResponseName = "realtime" + V1ServiceHealthResponseNameRest V1ServiceHealthResponseName = "rest" + V1ServiceHealthResponseNameStorage V1ServiceHealthResponseName = "storage" ) // Defines values for V1ServiceHealthResponseStatus. @@ -1008,14 +1010,55 @@ const ( N7day V1GetProjectUsageApiCountParamsInterval = "7day" ) +// Defines values for V1RemoveProjectAddonParamsAddonVariant0. +const ( + V1RemoveProjectAddonParamsAddonVariant0Ci12xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_12xlarge" + V1RemoveProjectAddonParamsAddonVariant0Ci16xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_16xlarge" + V1RemoveProjectAddonParamsAddonVariant0Ci24xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_24xlarge" + V1RemoveProjectAddonParamsAddonVariant0Ci24xlargeHighMemory V1RemoveProjectAddonParamsAddonVariant0 = "ci_24xlarge_high_memory" + V1RemoveProjectAddonParamsAddonVariant0Ci24xlargeOptimizedCpu V1RemoveProjectAddonParamsAddonVariant0 = "ci_24xlarge_optimized_cpu" + V1RemoveProjectAddonParamsAddonVariant0Ci24xlargeOptimizedMemory V1RemoveProjectAddonParamsAddonVariant0 = "ci_24xlarge_optimized_memory" + V1RemoveProjectAddonParamsAddonVariant0Ci2xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_2xlarge" + V1RemoveProjectAddonParamsAddonVariant0Ci48xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_48xlarge" + V1RemoveProjectAddonParamsAddonVariant0Ci48xlargeHighMemory V1RemoveProjectAddonParamsAddonVariant0 = "ci_48xlarge_high_memory" + V1RemoveProjectAddonParamsAddonVariant0Ci48xlargeOptimizedCpu V1RemoveProjectAddonParamsAddonVariant0 = "ci_48xlarge_optimized_cpu" + V1RemoveProjectAddonParamsAddonVariant0Ci48xlargeOptimizedMemory V1RemoveProjectAddonParamsAddonVariant0 = "ci_48xlarge_optimized_memory" + V1RemoveProjectAddonParamsAddonVariant0Ci4xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_4xlarge" + V1RemoveProjectAddonParamsAddonVariant0Ci8xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_8xlarge" + V1RemoveProjectAddonParamsAddonVariant0CiLarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_large" + V1RemoveProjectAddonParamsAddonVariant0CiMedium V1RemoveProjectAddonParamsAddonVariant0 = "ci_medium" + V1RemoveProjectAddonParamsAddonVariant0CiMicro V1RemoveProjectAddonParamsAddonVariant0 = "ci_micro" + V1RemoveProjectAddonParamsAddonVariant0CiSmall V1RemoveProjectAddonParamsAddonVariant0 = "ci_small" + V1RemoveProjectAddonParamsAddonVariant0CiXlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_xlarge" +) + +// Defines values for V1RemoveProjectAddonParamsAddonVariant1. +const ( + V1RemoveProjectAddonParamsAddonVariant1CdDefault V1RemoveProjectAddonParamsAddonVariant1 = "cd_default" +) + +// Defines values for V1RemoveProjectAddonParamsAddonVariant2. +const ( + V1RemoveProjectAddonParamsAddonVariant2Pitr14 V1RemoveProjectAddonParamsAddonVariant2 = "pitr_14" + V1RemoveProjectAddonParamsAddonVariant2Pitr28 V1RemoveProjectAddonParamsAddonVariant2 = "pitr_28" + V1RemoveProjectAddonParamsAddonVariant2Pitr7 V1RemoveProjectAddonParamsAddonVariant2 = "pitr_7" +) + +// Defines values for V1RemoveProjectAddonParamsAddonVariant3. +const ( + V1RemoveProjectAddonParamsAddonVariant3Ipv4Default V1RemoveProjectAddonParamsAddonVariant3 = "ipv4_default" +) + // Defines values for V1GetServicesHealthParamsServices. const ( - Auth V1GetServicesHealthParamsServices = "auth" - Db V1GetServicesHealthParamsServices = "db" - Pooler V1GetServicesHealthParamsServices = "pooler" - Realtime V1GetServicesHealthParamsServices = "realtime" - Rest V1GetServicesHealthParamsServices = "rest" - Storage V1GetServicesHealthParamsServices = "storage" + Auth V1GetServicesHealthParamsServices = "auth" + Db V1GetServicesHealthParamsServices = "db" + DbPostgresUser V1GetServicesHealthParamsServices = "db_postgres_user" + PgBouncer V1GetServicesHealthParamsServices = "pg_bouncer" + Pooler V1GetServicesHealthParamsServices = "pooler" + Realtime V1GetServicesHealthParamsServices = "realtime" + Rest V1GetServicesHealthParamsServices = "rest" + Storage V1GetServicesHealthParamsServices = "storage" ) // Defines values for V1ListAllSnippetsParamsSortBy. @@ -1343,6 +1386,7 @@ type BranchResponse struct { ReviewRequestedAt *time.Time `json:"review_requested_at,omitempty"` Status BranchResponseStatus `json:"status"` UpdatedAt time.Time `json:"updated_at"` + WithData bool `json:"with_data"` } // BranchResponseStatus defines model for BranchResponse.Status. @@ -3270,6 +3314,18 @@ type V1UpdateProjectApiKeyParams struct { Reveal *bool `form:"reveal,omitempty" json:"reveal,omitempty"` } +// V1RemoveProjectAddonParamsAddonVariant0 defines parameters for V1RemoveProjectAddon. +type V1RemoveProjectAddonParamsAddonVariant0 string + +// V1RemoveProjectAddonParamsAddonVariant1 defines parameters for V1RemoveProjectAddon. +type V1RemoveProjectAddonParamsAddonVariant1 string + +// V1RemoveProjectAddonParamsAddonVariant2 defines parameters for V1RemoveProjectAddon. +type V1RemoveProjectAddonParamsAddonVariant2 string + +// V1RemoveProjectAddonParamsAddonVariant3 defines parameters for V1RemoveProjectAddon. +type V1RemoveProjectAddonParamsAddonVariant3 string + // V1GetRestorePointParams defines parameters for V1GetRestorePoint. type V1GetRestorePointParams struct { Name *string `form:"name,omitempty" json:"name,omitempty"` diff --git a/pkg/config/apikeys.go b/pkg/config/apikeys.go new file mode 100644 index 000000000..35ed815d1 --- /dev/null +++ b/pkg/config/apikeys.go @@ -0,0 +1,169 @@ +package config + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "encoding/base64" + "math/big" + "time" + + "github.com/go-errors/errors" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +// generateAPIKeys generates JWT tokens using the appropriate signing method +func (a *auth) generateAPIKeys() error { + // Generate anon key if not provided + if len(a.AnonKey.Value) == 0 { + signed, err := a.generateJWT("anon") + if err != nil { + return err + } + a.AnonKey.Value = signed + } + // Generate service_role key if not provided + if len(a.ServiceRoleKey.Value) == 0 { + signed, err := a.generateJWT("service_role") + if err != nil { + return err + } + a.ServiceRoleKey.Value = signed + } + return nil +} + +func (a auth) generateJWT(role string) (string, error) { + claims := CustomClaims{Issuer: "supabase-demo", Role: role} + if len(a.SigningKeys) > 0 { + claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 365 * 10)) // 10 years + return generateAsymmetricJWT(a.SigningKeys[0], claims) + } + // Fallback to generating symmetric keys + if len(a.JwtSecret.Value) < 16 { + return "", errors.Errorf("Invalid config for auth.jwt_secret. Must be at least 16 characters") + } + signed, err := claims.NewToken().SignedString([]byte(a.JwtSecret.Value)) + if err != nil { + return "", errors.Errorf("failed to generate JWT: %w", err) + } + return signed, nil +} + +// generateAsymmetricJWT generates a JWT token signed with the provided JWK private key +func generateAsymmetricJWT(jwk JWK, claims CustomClaims) (string, error) { + privateKey, err := jwkToPrivateKey(jwk) + if err != nil { + return "", errors.Errorf("failed to convert JWK to private key: %w", err) + } + + // Determine signing method based on algorithm + var token *jwt.Token + switch jwk.Algorithm { + case AlgRS256: + token = jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + case AlgES256: + token = jwt.NewWithClaims(jwt.SigningMethodES256, claims) + default: + return "", errors.Errorf("unsupported algorithm: %s", jwk.Algorithm) + } + + if jwk.KeyID != uuid.Nil { + token.Header["kid"] = jwk.KeyID.String() + } + + tokenString, err := token.SignedString(privateKey) + if err != nil { + return "", errors.Errorf("failed to sign JWT: %w", err) + } + + return tokenString, nil +} + +// jwkToPrivateKey converts a JWK to a crypto.PrivateKey +func jwkToPrivateKey(jwk JWK) (crypto.PrivateKey, error) { + switch jwk.KeyType { + case "RSA": + return jwkToRSAPrivateKey(jwk) + case "EC": + return jwkToECDSAPrivateKey(jwk) + default: + return nil, errors.Errorf("unsupported key type: %s", jwk.KeyType) + } +} + +// jwkToRSAPrivateKey converts a JWK to an RSA private key +func jwkToRSAPrivateKey(jwk JWK) (*rsa.PrivateKey, error) { + nBytes, err := base64.RawURLEncoding.DecodeString(jwk.Modulus) + if err != nil { + return nil, errors.Errorf("failed to decode modulus: %w", err) + } + n := new(big.Int).SetBytes(nBytes) + + eBytes, err := base64.RawURLEncoding.DecodeString(jwk.Exponent) + if err != nil { + return nil, errors.Errorf("failed to decode exponent: %w", err) + } + e := int(new(big.Int).SetBytes(eBytes).Int64()) + + dBytes, err := base64.RawURLEncoding.DecodeString(jwk.PrivateExponent) + if err != nil { + return nil, errors.Errorf("failed to decode private exponent: %w", err) + } + d := new(big.Int).SetBytes(dBytes) + + pBytes, err := base64.RawURLEncoding.DecodeString(jwk.FirstPrimeFactor) + if err != nil { + return nil, errors.Errorf("failed to decode first prime factor: %w", err) + } + p := new(big.Int).SetBytes(pBytes) + + qBytes, err := base64.RawURLEncoding.DecodeString(jwk.SecondPrimeFactor) + if err != nil { + return nil, errors.Errorf("failed to decode second prime factor: %w", err) + } + q := new(big.Int).SetBytes(qBytes) + + return &rsa.PrivateKey{ + PublicKey: rsa.PublicKey{N: n, E: e}, + D: d, + Primes: []*big.Int{p, q}, + }, nil +} + +// jwkToECDSAPrivateKey converts a JWK to an ECDSA private key +func jwkToECDSAPrivateKey(jwk JWK) (*ecdsa.PrivateKey, error) { + // Only support P-256 curve for ES256 + if jwk.Curve != "P-256" { + return nil, errors.Errorf("unsupported curve: %s", jwk.Curve) + } + + xBytes, err := base64.RawURLEncoding.DecodeString(jwk.X) + if err != nil { + return nil, errors.Errorf("failed to decode x coordinate: %w", err) + } + x := new(big.Int).SetBytes(xBytes) + + yBytes, err := base64.RawURLEncoding.DecodeString(jwk.Y) + if err != nil { + return nil, errors.Errorf("failed to decode y coordinate: %w", err) + } + y := new(big.Int).SetBytes(yBytes) + + dBytes, err := base64.RawURLEncoding.DecodeString(jwk.PrivateExponent) + if err != nil { + return nil, errors.Errorf("failed to decode private key: %w", err) + } + d := new(big.Int).SetBytes(dBytes) + + return &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: x, + Y: y, + }, + D: d, + }, nil +} diff --git a/pkg/config/auth.go b/pkg/config/auth.go index 2b0e155d7..9dc0d3028 100644 --- a/pkg/config/auth.go +++ b/pkg/config/auth.go @@ -6,6 +6,7 @@ import ( "time" "github.com/go-errors/errors" + "github.com/google/uuid" "github.com/oapi-codegen/nullable" openapi_types "github.com/oapi-codegen/runtime/types" v1API "github.com/supabase/cli/pkg/api" @@ -69,6 +70,80 @@ func (p *CaptchaProvider) UnmarshalText(text []byte) error { return nil } +type Algorithm string + +const ( + AlgRS256 Algorithm = "RS256" + AlgES256 Algorithm = "ES256" +) + +func (p *Algorithm) UnmarshalText(text []byte) error { + allowed := []Algorithm{AlgRS256, AlgES256} + if *p = Algorithm(text); !sliceContains(allowed, *p) { + return errors.Errorf("must be one of %v", allowed) + } + return nil +} + +type JWK struct { + KeyType string `json:"kty"` + KeyID uuid.UUID `json:"kid,omitempty"` + Use string `json:"use,omitempty"` + KeyOps []string `json:"key_ops,omitempty"` + Algorithm Algorithm `json:"alg,omitempty"` + Extractable *bool `json:"ext,omitempty"` + // RSA specific fields + Modulus string `json:"n,omitempty"` + Exponent string `json:"e,omitempty"` + // RSA private key fields + PrivateExponent string `json:"d,omitempty"` + FirstPrimeFactor string `json:"p,omitempty"` + SecondPrimeFactor string `json:"q,omitempty"` + FirstFactorCRTExponent string `json:"dp,omitempty"` + SecondFactorCRTExponent string `json:"dq,omitempty"` + FirstCRTCoefficient string `json:"qi,omitempty"` + // EC specific fields + Curve string `json:"crv,omitempty"` + X string `json:"x,omitempty"` + Y string `json:"y,omitempty"` +} + +// ToPublicJWK converts a JWK to a public-only version by removing private key components +func (j JWK) ToPublicJWK() JWK { + publicJWK := JWK{ + KeyType: j.KeyType, + KeyID: j.KeyID, + Use: j.Use, + Algorithm: j.Algorithm, + } + + // Copy the underlying type instead of the pointer + if j.Extractable != nil { + publicJWK.Extractable = cast.Ptr(*j.Extractable) + } + + // Only include key_ops for verification (not signing) for public keys + for _, op := range j.KeyOps { + if op == "verify" { + publicJWK.KeyOps = append(publicJWK.KeyOps, op) + } + } + + switch j.KeyType { + case "RSA": + // Include only public key components for RSA + publicJWK.Modulus = j.Modulus + publicJWK.Exponent = j.Exponent + case "EC": + // Include only public key components for ECDSA + publicJWK.Curve = j.Curve + publicJWK.X = j.X + publicJWK.Y = j.Y + } + + return publicJWK +} + type ( auth struct { Enabled bool `toml:"enabled"` @@ -85,6 +160,7 @@ type ( MinimumPasswordLength uint `toml:"minimum_password_length"` PasswordRequirements PasswordRequirements `toml:"password_requirements"` SigningKeysPath string `toml:"signing_keys_path"` + SigningKeys []JWK `toml:"-"` RateLimit rateLimit `toml:"rate_limit"` Captcha *captcha `toml:"captcha"` @@ -111,6 +187,7 @@ type ( Auth0 tpaAuth0 `toml:"auth0"` Cognito tpaCognito `toml:"aws_cognito"` Clerk tpaClerk `toml:"clerk"` + WorkOs tpaWorkOs `toml:"workos"` } rateLimit struct { @@ -149,6 +226,12 @@ type ( Domain string `toml:"domain"` } + tpaWorkOs struct { + Enabled bool `toml:"enabled"` + + IssuerUrl string `toml:"issuer_url"` + } + email struct { EnableSignup bool `toml:"enable_signup"` DoubleConfirmChanges bool `toml:"double_confirm_changes"` diff --git a/pkg/config/config.go b/pkg/config/config.go index d96cc3952..3703acc3b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -585,26 +585,6 @@ func (c *config) Load(path string, fsys fs.FS) error { if err := c.loadFromFile(builder.ConfigPath, fsys); err != nil { return err } - // Generate JWT tokens - if len(c.Auth.JwtSecret.Value) < 16 { - return errors.Errorf("Invalid config for auth.jwt_secret. Must be at least 16 characters") - } - if len(c.Auth.AnonKey.Value) == 0 { - anonToken := CustomClaims{Role: "anon"}.NewToken() - if signed, err := anonToken.SignedString([]byte(c.Auth.JwtSecret.Value)); err != nil { - return errors.Errorf("failed to generate anon key: %w", err) - } else { - c.Auth.AnonKey.Value = signed - } - } - if len(c.Auth.ServiceRoleKey.Value) == 0 { - anonToken := CustomClaims{Role: "service_role"}.NewToken() - if signed, err := anonToken.SignedString([]byte(c.Auth.JwtSecret.Value)); err != nil { - return errors.Errorf("failed to generate service_role key: %w", err) - } else { - c.Auth.ServiceRoleKey.Value = signed - } - } // TODO: move linked pooler connection string elsewhere if connString, err := fs.ReadFile(fsys, builder.PoolerUrlPath); err == nil && len(connString) > 0 { c.Db.Pooler.ConnectionString = string(connString) @@ -783,11 +763,14 @@ func (c *config) Validate(fsys fs.FS) error { return errors.New("Missing required field in config: db.major_version") case 12: return errors.New("Postgres version 12.x is unsupported. To use the CLI, either start a new project or follow project migration steps here: https://supabase.com/docs/guides/database#migrating-between-projects.") - case 13, 14, 17: - // TODO: support oriole db 17 eventually - case 15: + case 13, 14: + case 15, 17: if len(c.Experimental.OrioleDBVersion) > 0 { - c.Db.Image = "supabase/postgres:orioledb-" + c.Experimental.OrioleDBVersion + if VersionCompare(c.Experimental.OrioleDBVersion, "15.1.1.13") > 0 { + c.Db.Image = fmt.Sprintf("supabase/postgres:%s-orioledb", c.Experimental.OrioleDBVersion) + } else { + c.Db.Image = "supabase/postgres:orioledb-" + c.Experimental.OrioleDBVersion + } if err := assertEnvLoaded(c.Experimental.S3Host); err != nil { return err } @@ -851,6 +834,18 @@ func (c *config) Validate(fsys fs.FS) error { return err } } + if len(c.Auth.SigningKeysPath) > 0 { + if f, err := fsys.Open(c.Auth.SigningKeysPath); errors.Is(err, os.ErrNotExist) { + // Ignore missing signing key path on CI + } else if err != nil { + return errors.Errorf("failed to read signing keys: %w", err) + } else if c.Auth.SigningKeys, err = fetcher.ParseJSON[[]JWK](f); err != nil { + return errors.Errorf("failed to decode signing keys: %w", err) + } + } + if err := c.Auth.generateAPIKeys(); err != nil { + return err + } if err := c.Auth.Hook.validate(); err != nil { return err } @@ -880,9 +875,10 @@ func (c *config) Validate(fsys fs.FS) error { case 0: return errors.New("Missing required field in config: edge_runtime.deno_version") case 1: + c.EdgeRuntime.Image = deno1 break case 2: - c.EdgeRuntime.Image = deno2 + break default: return errors.Errorf("Failed reading config: Invalid %s: %v.", "edge_runtime.deno_version", c.EdgeRuntime.DenoVersion) } @@ -1319,6 +1315,17 @@ func (c *tpaClerk) validate() (err error) { return nil } +func (w *tpaWorkOs) validate() error { + if w.IssuerUrl == "" { + return errors.New("Invalid config: auth.third_party.workos is enabled but without a issuer_url.") + } + return nil +} + +func (w *tpaWorkOs) issuerURL() string { + return w.IssuerUrl +} + func (tpa *thirdParty) validate() error { enabled := 0 @@ -1354,6 +1361,14 @@ func (tpa *thirdParty) validate() error { } } + if tpa.WorkOs.Enabled { + enabled += 1 + + if err := tpa.WorkOs.validate(); err != nil { + return err + } + } + if enabled > 1 { return errors.New("Invalid config: Only one third_party provider allowed to be enabled at a time.") } @@ -1378,6 +1393,10 @@ func (tpa *thirdParty) IssuerURL() string { return tpa.Clerk.issuerURL() } + if tpa.WorkOs.Enabled { + return tpa.WorkOs.issuerURL() + } + return "" } @@ -1449,19 +1468,16 @@ func (a *auth) ResolveJWKS(ctx context.Context, fsys afero.Fs) (string, error) { jwks.Keys = append(jwks.Keys, rJWKS.Keys...) } - // If SIGNING_KEYS_PATH is provided, read from file - if len(a.SigningKeysPath) > 0 { - f, err := fsys.Open(a.SigningKeysPath) + // Convert each signing key to public-only version + for _, key := range a.SigningKeys { + publicKeyEncoded, err := json.Marshal(key.ToPublicJWK()) if err != nil { - return "", errors.Errorf("failed to read signing key: %w", err) + return "", errors.Errorf("failed to marshal public key: %w", err) } - jwtKeysArray, err := fetcher.ParseJSON[[]json.RawMessage](f) - if err != nil { - return "", err - } - jwks.Keys = append(jwks.Keys, jwtKeysArray...) - } else { - // Fallback to JWT_SECRET for backward compatibility + jwks.Keys = append(jwks.Keys, json.RawMessage(publicKeyEncoded)) + } + // Fallback to JWT_SECRET for backward compatibility + if len(a.SigningKeys) == 0 { jwtSecret := secretJWK{ KeyType: "oct", KeyBase64URL: base64.RawURLEncoding.EncodeToString([]byte(a.JwtSecret.Value)), diff --git a/pkg/config/constants.go b/pkg/config/constants.go index b9d03e3e3..466fbe3eb 100644 --- a/pkg/config/constants.go +++ b/pkg/config/constants.go @@ -12,7 +12,7 @@ const ( pg13 = "supabase/postgres:13.3.0" pg14 = "supabase/postgres:14.1.0.89" pg15 = "supabase/postgres:15.8.1.085" - deno2 = "supabase/edge-runtime:v1.68.0-develop.18" + deno1 = "supabase/edge-runtime:v1.68.3" ) type images struct { diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index 0038d3dd4..4213074e5 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -1,19 +1,19 @@ # Exposed for updates by .github/dependabot.yml -FROM supabase/postgres:17.4.1.072 AS pg +FROM supabase/postgres:17.4.1.074 AS pg # Append to ServiceImages when adding new dependencies below FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit FROM postgrest/postgrest:v13.0.4 AS postgrest FROM supabase/postgres-meta:v0.91.5 AS pgmeta -FROM supabase/studio:2025.08.04-sha-6e99ca6 AS studio +FROM supabase/studio:2025.08.18-sha-c153df2 AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy -FROM supabase/edge-runtime:v1.68.3 AS edgeruntime +FROM supabase/edge-runtime:v1.69.1 AS edgeruntime FROM timberio/vector:0.28.1-alpine AS vector FROM supabase/supavisor:2.6.1 AS supavisor FROM supabase/gotrue:v2.178.0 AS gotrue -FROM supabase/realtime:v2.41.23 AS realtime -FROM supabase/storage-api:v1.26.3 AS storage -FROM supabase/logflare:1.18.3 AS logflare +FROM supabase/realtime:v2.43.0 AS realtime +FROM supabase/storage-api:v1.26.4 AS storage +FROM supabase/logflare:1.18.4 AS logflare # Append to JobImages when adding new dependencies below FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ FROM supabase/migra:3.0.1663481299 AS migra diff --git a/pkg/config/templates/config.toml b/pkg/config/templates/config.toml index fea0c284d..382217dee 100644 --- a/pkg/config/templates/config.toml +++ b/pkg/config/templates/config.toml @@ -303,13 +303,14 @@ enabled = false [edge_runtime] enabled = true -# Configure one of the supported request policies: `oneshot`, `per_worker`. -# Use `oneshot` for hot reload, or `per_worker` for load testing. -policy = "oneshot" +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" # Port to attach the Chrome inspector for debugging edge functions. inspector_port = 8083 # The Deno major version to use. -deno_version = 1 +deno_version = 2 # [edge_runtime.secrets] # secret_key = "env(SECRET_VALUE)" diff --git a/pkg/config/updater.go b/pkg/config/updater.go index 96b73efd2..4e2159488 100644 --- a/pkg/config/updater.go +++ b/pkg/config/updater.go @@ -6,6 +6,7 @@ import ( "os" "github.com/go-errors/errors" + "github.com/google/uuid" v1API "github.com/supabase/cli/pkg/api" ) @@ -163,6 +164,77 @@ func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, return nil } +func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string, signingKeys []JWK, filter ...func(string) bool) error { + if len(signingKeys) == 0 { + return nil + } + resp, err := u.client.V1GetProjectSigningKeysWithResponse(ctx, projectRef) + if err != nil { + return errors.Errorf("failed to fetch signing keys: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body)) + } + exists := map[uuid.UUID]struct{}{} + for _, k := range resp.JSON200.Keys { + if k.PublicJwk != nil { + exists[k.Id] = struct{}{} + } + } + var toInsert []JWK + for _, k := range signingKeys { + if _, ok := exists[k.KeyID]; !ok { + toInsert = append(toInsert, k) + } + } + if len(toInsert) == 0 { + fmt.Fprintln(os.Stderr, "Remote JWT signing keys are up to date.") + return nil + } + fmt.Fprintln(os.Stderr, "JWT signing keys to insert:") + for _, k := range toInsert { + fmt.Fprintln(os.Stderr, " -", k.KeyID) + } + for _, keep := range filter { + if !keep("signing keys") { + return nil + } + } + for _, k := range toInsert { + body := v1API.CreateSigningKeyBody{ + Algorithm: v1API.CreateSigningKeyBodyAlgorithm(k.Algorithm), + PrivateJwk: &v1API.CreateSigningKeyBody_PrivateJwk{}, + } + switch k.Algorithm { + case AlgRS256: + body.PrivateJwk.FromCreateSigningKeyBodyPrivateJwk0(v1API.CreateSigningKeyBodyPrivateJwk0{ + D: k.PrivateExponent, + Dp: k.FirstFactorCRTExponent, + Dq: k.SecondFactorCRTExponent, + E: v1API.CreateSigningKeyBodyPrivateJwk0E(k.Exponent), + Kty: v1API.CreateSigningKeyBodyPrivateJwk0Kty(k.KeyType), + N: k.Modulus, + P: k.FirstPrimeFactor, + Q: k.SecondPrimeFactor, + Qi: k.FirstCRTCoefficient, + }) + case AlgES256: + body.PrivateJwk.FromCreateSigningKeyBodyPrivateJwk1(v1API.CreateSigningKeyBodyPrivateJwk1{ + Crv: v1API.CreateSigningKeyBodyPrivateJwk1Crv(k.Curve), + D: k.PrivateExponent, + Kty: v1API.CreateSigningKeyBodyPrivateJwk1Kty(k.KeyType), + X: k.X, + Y: k.Y, + }) + } + if resp, err := u.client.V1CreateProjectSigningKeyWithResponse(ctx, projectRef, body); err != nil { + return errors.Errorf("failed to add signing key: %w", err) + } else if status := resp.StatusCode(); status < 200 || status >= 300 { + return errors.Errorf("unexpected status %d: %s", status, string(resp.Body)) + } + } + return nil +} + func (u *ConfigUpdater) UpdateStorageConfig(ctx context.Context, projectRef string, c storage, filter ...func(string) bool) error { if !c.Enabled { return nil diff --git a/pkg/function/batch.go b/pkg/function/batch.go index 69754f978..a98e0d5ed 100644 --- a/pkg/function/batch.go +++ b/pkg/function/batch.go @@ -16,7 +16,6 @@ import ( "github.com/docker/go-units" "github.com/go-errors/errors" "github.com/supabase/cli/pkg/api" - "github.com/supabase/cli/pkg/cast" "github.com/supabase/cli/pkg/config" ) @@ -44,15 +43,15 @@ func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig con return err } policy.Reset() - checksum := make(map[string]string, len(result)) - for _, f := range result { - checksum[f.Slug] = cast.Val(f.EzbrSha256, "") + slugToIndex := make(map[string]int, len(result)) + for i, f := range result { + slugToIndex[f.Slug] = i } var toUpdate api.BulkUpdateFunctionBody OUTER: for slug, function := range functionConfig { if !function.Enabled { - fmt.Fprintln(os.Stderr, "Skipped deploying Function:", slug) + fmt.Fprintln(os.Stderr, "Skipping disabled Function:", slug) continue } for _, keep := range filter { @@ -69,13 +68,15 @@ OUTER: bodyHash := sha256.Sum256(body.Bytes()) meta.SHA256 = hex.EncodeToString(bodyHash[:]) // Skip if function has not changed - if checksum[slug] == meta.SHA256 { + if i, exists := slugToIndex[slug]; exists && i >= 0 && + result[i].EzbrSha256 != nil && *result[i].EzbrSha256 == meta.SHA256 && + result[i].VerifyJwt != nil && *result[i].VerifyJwt == function.VerifyJWT { fmt.Fprintln(os.Stderr, "No change found in Function:", slug) continue } // Update if function already exists upsert := func() (api.BulkUpdateFunctionBody, error) { - if _, ok := checksum[slug]; ok { + if _, exists := slugToIndex[slug]; exists { return s.updateFunction(ctx, slug, meta, bytes.NewReader(body.Bytes())) } return s.createFunction(ctx, slug, meta, bytes.NewReader(body.Bytes())) @@ -84,7 +85,7 @@ OUTER: fmt.Fprintf(os.Stderr, "Deploying Function: %s (script size: %s)\n", slug, functionSize) result, err := backoff.RetryNotifyWithData(upsert, policy, func(err error, d time.Duration) { if strings.Contains(err.Error(), "Duplicated function slug") { - checksum[slug] = "" + slugToIndex[slug] = -1 } }) if err != nil { diff --git a/pkg/function/batch_test.go b/pkg/function/batch_test.go index cc6437ec3..d47b28be4 100644 --- a/pkg/function/batch_test.go +++ b/pkg/function/batch_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/supabase/cli/pkg/api" + "github.com/supabase/cli/pkg/cast" "github.com/supabase/cli/pkg/config" ) @@ -29,19 +30,24 @@ func (b *MockBundler) Bundle(ctx context.Context, slug, entrypoint, importMap st }, nil } +func mockClient(t *testing.T) EdgeRuntimeAPI { + apiClient, err := api.NewClientWithResponses(mockApiHost) + require.NoError(t, err) + return NewEdgeRuntimeAPI(mockProject, *apiClient, func(era *EdgeRuntimeAPI) { + era.eszip = &MockBundler{} + }) +} + const ( mockApiHost = "https://api.supabase.com" mockProject = "test-project" ) func TestUpsertFunctions(t *testing.T) { - apiClient, err := api.NewClientWithResponses(mockApiHost) - require.NoError(t, err) - client := NewEdgeRuntimeAPI(mockProject, *apiClient, func(era *EdgeRuntimeAPI) { - era.eszip = &MockBundler{} - }) + client := mockClient(t) t.Run("deploys with bulk update", func(t *testing.T) { + // t.Cleanup(mockClock(100 * time.Millisecond)) // Setup mock api defer gock.OffAll() gock.New(mockApiHost). @@ -53,8 +59,8 @@ func TestUpsertFunctions(t *testing.T) { Reply(http.StatusOK). JSON(api.FunctionResponse{Slug: "test-a"}) gock.New(mockApiHost). - Post("/v1/projects/" + mockProject + "/functions/test-b"). - Reply(http.StatusOK). + Post("/v1/projects/" + mockProject + "/functions"). + Reply(http.StatusCreated). JSON(api.FunctionResponse{Slug: "test-b"}) gock.New(mockApiHost). Put("/v1/projects/" + mockProject + "/functions"). @@ -68,8 +74,30 @@ func TestUpsertFunctions(t *testing.T) { JSON(api.V1BulkUpdateFunctionsResponse{}) // Run test err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ - "test-a": {}, - "test-b": {}, + "test-a": {Enabled: true}, + "test-b": {Enabled: true}, + }) + // Check error + assert.NoError(t, err) + assert.Empty(t, gock.Pending()) + assert.Empty(t, gock.GetUnmatchedRequests()) + }) + + t.Run("skips disabled and unchanged", func(t *testing.T) { + // Setup mock api + defer gock.OffAll() + gock.New(mockApiHost). + Get("/v1/projects/" + mockProject + "/functions"). + Reply(http.StatusOK). + JSON([]api.FunctionResponse{{ + Slug: "test-a", + VerifyJwt: cast.Ptr(true), + EzbrSha256: cast.Ptr("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), + }}) + // Run test + err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ + "test-a": {Enabled: true, VerifyJWT: true}, + "test-b": {Enabled: false}, }) // Check error assert.NoError(t, err) @@ -94,7 +122,7 @@ func TestUpsertFunctions(t *testing.T) { JSON(api.FunctionResponse{Slug: "test"}) // Run test err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ - "test": {}, + "test": {Enabled: true}, }) // Check error assert.NoError(t, err) @@ -121,58 +149,60 @@ func TestUpsertFunctions(t *testing.T) { assert.Empty(t, gock.Pending()) assert.Empty(t, gock.GetUnmatchedRequests()) }) +} - t.Run("retries on create failure", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New(mockApiHost). - Get("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusOK). - JSON([]api.FunctionResponse{}) - gock.New(mockApiHost). - Post("/v1/projects/" + mockProject + "/functions"). - ReplyError(errors.New("network error")) - gock.New(mockApiHost). - Post("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusServiceUnavailable) - gock.New(mockApiHost). - Post("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusCreated). - JSON(api.FunctionResponse{Slug: "test"}) - // Run test - err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ - "test": {}, - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) +func TestCreateFunction(t *testing.T) { + client := mockClient(t) + // Setup mock api + defer gock.OffAll() + gock.New(mockApiHost). + Get("/v1/projects/" + mockProject + "/functions"). + Reply(http.StatusOK). + JSON([]api.FunctionResponse{}) + gock.New(mockApiHost). + Post("/v1/projects/" + mockProject + "/functions"). + ReplyError(errors.New("network error")) + gock.New(mockApiHost). + Post("/v1/projects/" + mockProject + "/functions"). + Reply(http.StatusServiceUnavailable) + gock.New(mockApiHost). + Post("/v1/projects/" + mockProject + "/functions"). + Reply(http.StatusCreated). + JSON(api.FunctionResponse{Slug: "test"}) + // Run test + err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ + "test": {Enabled: true}, }) + // Check error + assert.NoError(t, err) + assert.Empty(t, gock.Pending()) + assert.Empty(t, gock.GetUnmatchedRequests()) +} - t.Run("retries on update failure", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() - gock.New(mockApiHost). - Get("/v1/projects/" + mockProject + "/functions"). - Reply(http.StatusOK). - JSON([]api.FunctionResponse{{Slug: "test"}}) - gock.New(mockApiHost). - Patch("/v1/projects/" + mockProject + "/functions/test"). - ReplyError(errors.New("network error")) - gock.New(mockApiHost). - Patch("/v1/projects/" + mockProject + "/functions/test"). - Reply(http.StatusServiceUnavailable) - gock.New(mockApiHost). - Patch("/v1/projects/" + mockProject + "/functions/test"). - Reply(http.StatusOK). - JSON(api.FunctionResponse{Slug: "test"}) - // Run test - err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ - "test": {}, - }) - // Check error - assert.NoError(t, err) - assert.Empty(t, gock.Pending()) - assert.Empty(t, gock.GetUnmatchedRequests()) +func TestUpdateFunction(t *testing.T) { + client := mockClient(t) + // Setup mock api + defer gock.OffAll() + gock.New(mockApiHost). + Get("/v1/projects/" + mockProject + "/functions"). + Reply(http.StatusOK). + JSON([]api.FunctionResponse{{Slug: "test"}}) + gock.New(mockApiHost). + Patch("/v1/projects/" + mockProject + "/functions/test"). + ReplyError(errors.New("network error")) + gock.New(mockApiHost). + Patch("/v1/projects/" + mockProject + "/functions/test"). + Reply(http.StatusServiceUnavailable) + gock.New(mockApiHost). + Patch("/v1/projects/" + mockProject + "/functions/test"). + Reply(http.StatusOK). + JSON(api.FunctionResponse{Slug: "test"}) + // Run test + err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ + "test": {Enabled: true}, }) + // Check error + assert.NoError(t, err) + assert.Empty(t, gock.Pending()) + assert.Empty(t, gock.GetUnmatchedRequests()) } diff --git a/pkg/function/deploy.go b/pkg/function/deploy.go index d2e302deb..88e2f464a 100644 --- a/pkg/function/deploy.go +++ b/pkg/function/deploy.go @@ -27,7 +27,7 @@ func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.Funct var toDeploy []FunctionDeployMetadata for slug, fc := range functionConfig { if !fc.Enabled { - fmt.Fprintln(os.Stderr, "Skipped deploying Function:", slug) + fmt.Fprintln(os.Stderr, "Skipping disabled Function:", slug) continue } meta := FunctionDeployMetadata{ diff --git a/pkg/go.mod b/pkg/go.mod index e913dce72..4270c4fbb 100644 --- a/pkg/go.mod +++ b/pkg/go.mod @@ -11,6 +11,7 @@ require ( github.com/go-errors/errors v1.5.1 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/google/uuid v1.6.0 github.com/h2non/gock v1.2.0 github.com/jackc/pgconn v1.14.3 github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 @@ -23,10 +24,10 @@ require ( github.com/oapi-codegen/runtime v1.1.2 github.com/spf13/afero v1.14.0 github.com/spf13/viper v1.20.1 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.0 github.com/tidwall/jsonc v0.3.2 golang.org/x/mod v0.27.0 - google.golang.org/grpc v1.74.2 + google.golang.org/grpc v1.75.0 ) require ( @@ -35,7 +36,6 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/ethereum/go-ethereum v1.15.8 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgio v1.0.0 // indirect @@ -48,13 +48,13 @@ require ( github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/pflag v1.0.7 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.38.0 // indirect + golang.org/x/crypto v0.39.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/text v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/go.sum b/pkg/go.sum index ee04f3436..59b2f80ba 100644 --- a/pkg/go.sum +++ b/pkg/go.sum @@ -162,8 +162,8 @@ github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= @@ -182,8 +182,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= +github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc= @@ -219,8 +219,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -274,8 +274,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -292,8 +292,8 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= -google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/migration/dump.go b/pkg/migration/dump.go index 66d3f7486..07adcc816 100644 --- a/pkg/migration/dump.go +++ b/pkg/migration/dump.go @@ -93,6 +93,7 @@ var ( "service_role", "supabase_admin", "supabase_auth_admin", + "supabase_etl_admin", "supabase_functions_admin", "supabase_read_only_user", "supabase_realtime_admin", diff --git a/pkg/pgxv5/connect.go b/pkg/pgxv5/connect.go index daacb5fc0..d32afca5b 100644 --- a/pkg/pgxv5/connect.go +++ b/pkg/pgxv5/connect.go @@ -24,7 +24,9 @@ func Connect(ctx context.Context, connString string, options ...func(*pgx.ConnCo return nil, errors.Errorf("failed to parse connection string: %w", err) } config.OnNotice = func(pc *pgconn.PgConn, n *pgconn.Notice) { - fmt.Fprintf(os.Stderr, "%s (%s): %s\n", n.Severity, n.Code, n.Message) + if !shouldIgnore(n.Message) { + fmt.Fprintf(os.Stderr, "%s (%s): %s\n", n.Severity, n.Code, n.Message) + } } if strings.HasPrefix(config.User, CLI_LOGIN_ROLE) { config.AfterConnect = func(ctx context.Context, pgconn *pgconn.PgConn) error { @@ -42,3 +44,9 @@ func Connect(ctx context.Context, connString string, options ...func(*pgx.ConnCo } return conn, nil } + +func shouldIgnore(msg string) bool { + return strings.Contains(msg, `schema "supabase_migrations" already exists`) || + strings.Contains(msg, `relation "schema_migrations" already exists`) || + strings.Contains(msg, `relation "seed_files" already exists`) +}