diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ddfd3ef5d..67deb0d279 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: go tool gotestsum -- -race -v -count=1 ./... \ -coverpkg="./cmd/...,./internal/...,${pkgs}" -coverprofile=coverage.out - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: code-coverage-report path: coverage.out @@ -39,7 +39,7 @@ jobs: - test runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: code-coverage-report - uses: coverallsapp/github-action@v2 @@ -63,6 +63,7 @@ jobs: with: args: --timeout 3m --verbose version: latest + only-new-issues: true start: name: Start @@ -76,12 +77,19 @@ jobs: - run: go build main.go - run: ./main init - run: sed -i '/\[db.pooler\]/{n;s/.*/enabled = true/}' supabase/config.toml - - run: ./main start + - run: ./main start --workdir tests env: SUPABASE_INTERNAL_IMAGE_REGISTRY: ghcr.io + - name: Install websocat + run: | + sudo wget -qO /usr/local/bin/websocat https://github.com/vi/websocat/releases/latest/download/websocat.x86_64-unknown-linux-musl + sudo chmod a+x /usr/local/bin/websocat + websocat --version + - run: ./e2e-test.sh ./main link: name: Link + if: ${{ !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index a28df0d176..8b7296ea45 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -30,7 +30,7 @@ jobs: mv tmp.$$.json package.json npm pack - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: installer path: supabase-1.28.0.tgz @@ -43,7 +43,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: installer @@ -59,7 +59,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: installer @@ -75,7 +75,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: installer @@ -98,7 +98,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: installer @@ -117,7 +117,7 @@ jobs: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: installer diff --git a/.github/workflows/mirror-image.yml b/.github/workflows/mirror-image.yml index abf168796b..1cd9e2d949 100644 --- a/.github/workflows/mirror-image.yml +++ b/.github/workflows/mirror-image.yml @@ -30,14 +30,14 @@ jobs: TAG=${{ github.event.client_payload.image || inputs.image }} echo "image=${TAG##*/}" >> $GITHUB_OUTPUT - name: configure aws credentials - uses: aws-actions/configure-aws-credentials@v5.1.1 + uses: aws-actions/configure-aws-credentials@v6.0.0 with: role-to-assume: ${{ secrets.PROD_AWS_ROLE }} aws-region: us-east-1 - - uses: docker/login-action@v3 + - uses: docker/login-action@v4 with: registry: public.ecr.aws - - uses: docker/login-action@v3 + - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/pg-prove.yml b/.github/workflows/pg-prove.yml index 052ac65832..83812a58ee 100644 --- a/.github/workflows/pg-prove.yml +++ b/.github/workflows/pg-prove.yml @@ -12,8 +12,8 @@ jobs: outputs: image_tag: supabase/pg_prove:${{ steps.version.outputs.pg_prove }} steps: - - uses: docker/setup-buildx-action@v3 - - uses: docker/build-push-action@v6 + - uses: docker/setup-buildx-action@v4 + - uses: docker/build-push-action@v7 with: load: true context: https://github.com/horrendo/pg_prove.git @@ -43,15 +43,15 @@ jobs: image_digest: ${{ steps.build.outputs.digest }} steps: - run: docker context create builders - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@v4 with: endpoint: builders - - uses: docker/login-action@v3 + - uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: push: true context: https://github.com/horrendo/pg_prove.git @@ -66,8 +66,8 @@ jobs: - build_image runs-on: ubuntu-latest steps: - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/publish-migra.yml b/.github/workflows/publish-migra.yml index b4a2625c54..e0223a6d60 100644 --- a/.github/workflows/publish-migra.yml +++ b/.github/workflows/publish-migra.yml @@ -12,8 +12,8 @@ jobs: outputs: image_tag: supabase/migra:${{ steps.version.outputs.migra }} steps: - - uses: docker/setup-buildx-action@v3 - - uses: docker/build-push-action@v6 + - uses: docker/setup-buildx-action@v4 + - uses: docker/build-push-action@v7 with: load: true context: https://github.com/djrobstep/migra.git @@ -43,15 +43,15 @@ jobs: image_digest: ${{ steps.build.outputs.digest }} steps: - run: docker context create builders - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@v4 with: endpoint: builders - - uses: docker/login-action@v3 + - uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: push: true context: https://github.com/djrobstep/migra.git @@ -66,8 +66,8 @@ jobs: - build_image runs-on: ubuntu-latest steps: - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index fca87826e8..b354f785d0 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -44,7 +44,7 @@ jobs: go-version-file: go.mod cache: true - - uses: goreleaser/goreleaser-action@v6 + - uses: goreleaser/goreleaser-action@v7 with: distribution: goreleaser version: ~> v2 diff --git a/README.md b/README.md index 71b140c741..da6cedb3d1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Supabase CLI -[![Coverage Status](https://coveralls.io/repos/github/supabase/cli/badge.svg?branch=main)](https://coveralls.io/github/supabase/cli?branch=main) [![Bitbucket Pipelines](https://img.shields.io/bitbucket/pipelines/supabase-cli/setup-cli/master?style=flat-square&label=Bitbucket%20Canary)](https://bitbucket.org/supabase-cli/setup-cli/pipelines) [![Gitlab Pipeline Status](https://img.shields.io/gitlab/pipeline-status/sweatybridge%2Fsetup-cli?label=Gitlab%20Canary) +[![Coverage Status](https://coveralls.io/repos/github/supabase/cli/badge.svg?branch=develop)](https://coveralls.io/github/supabase/cli?branch=develop) [![Bitbucket Pipelines](https://img.shields.io/bitbucket/pipelines/supabase-cli/setup-cli/master?style=flat-square&label=Bitbucket%20Canary)](https://bitbucket.org/supabase-cli/setup-cli/pipelines) [![Gitlab Pipeline Status](https://img.shields.io/gitlab/pipeline-status/sweatybridge%2Fsetup-cli?label=Gitlab%20Canary) ](https://gitlab.com/sweatybridge/setup-cli/-/pipelines) [Supabase](https://supabase.io) is an open source Firebase alternative. We're building the features of Firebase using enterprise-grade open source tools. diff --git a/api/overlay.yaml b/api/overlay.yaml index 9418cccd2f..7843f3d86b 100644 --- a/api/overlay.yaml +++ b/api/overlay.yaml @@ -38,6 +38,9 @@ actions: - target: $.components.schemas.*.properties.private_jwk.discriminator description: Replaces discriminated union with concrete type remove: true +- target: $.components.schemas.DiskRequestBody.properties.attributes.discriminator + description: Replaces discriminated union with concrete type + remove: true - target: $.paths.*.*.parameters[?(@.name=='branch_id_or_ref')] update: schema: diff --git a/cmd/branches.go b/cmd/branches.go index 1f12f8727b..f0888b3429 100644 --- a/cmd/branches.go +++ b/cmd/branches.go @@ -16,7 +16,6 @@ import ( "github.com/supabase/cli/internal/branches/pause" "github.com/supabase/cli/internal/branches/unpause" "github.com/supabase/cli/internal/branches/update" - "github.com/supabase/cli/internal/gen/keys" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" "github.com/supabase/cli/pkg/api" @@ -229,7 +228,7 @@ func promptBranchId(ctx context.Context, fsys afero.Fs) error { if console := utils.NewConsole(); !console.IsTTY { // Only read from stdin if the terminal is non-interactive title := "Enter the name of your branch" - if branchId = keys.GetGitBranch(fsys); len(branchId) > 0 { + if branchId = utils.GetGitBranch(fsys); len(branchId) > 0 { title += fmt.Sprintf(" (or leave blank to use %s)", utils.Aqua(branchId)) } title += ": " diff --git a/cmd/db.go b/cmd/db.go index 94af929082..409ef42380 100644 --- a/cmd/db.go +++ b/cmd/db.go @@ -8,10 +8,6 @@ import ( "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/supabase/cli/internal/db/branch/create" - "github.com/supabase/cli/internal/db/branch/delete" - "github.com/supabase/cli/internal/db/branch/list" - "github.com/supabase/cli/internal/db/branch/switch_" "github.com/supabase/cli/internal/db/diff" "github.com/supabase/cli/internal/db/dump" "github.com/supabase/cli/internal/db/lint" @@ -22,6 +18,11 @@ import ( "github.com/supabase/cli/internal/db/test" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/legacy/branch/create" + "github.com/supabase/cli/legacy/branch/delete" + "github.com/supabase/cli/legacy/branch/list" + "github.com/supabase/cli/legacy/branch/switch_" + legacy "github.com/supabase/cli/legacy/diff" "github.com/supabase/cli/pkg/migration" ) @@ -90,7 +91,7 @@ var ( Short: "Diffs the local database for schema changes", RunE: func(cmd *cobra.Command, args []string) error { if usePgAdmin { - return diff.RunPgAdmin(cmd.Context(), schema, file, flags.DbConfig, afero.NewOsFs()) + return legacy.RunPgAdmin(cmd.Context(), schema, file, flags.DbConfig, afero.NewOsFs()) } differ := diff.DiffSchemaMigra if usePgSchema { diff --git a/cmd/domains.go b/cmd/domains.go index 9ec4a097da..0d30637478 100644 --- a/cmd/domains.go +++ b/cmd/domains.go @@ -8,6 +8,7 @@ import ( "github.com/supabase/cli/internal/hostnames/delete" "github.com/supabase/cli/internal/hostnames/get" "github.com/supabase/cli/internal/hostnames/reverify" + "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" ) @@ -32,7 +33,10 @@ Use of custom domains and vanity subdomains is mutually exclusive. Expects your custom hostname to have a CNAME record to your Supabase project's subdomain.`, RunE: func(cmd *cobra.Command, args []string) error { - return create.Run(cmd.Context(), flags.ProjectRef, customHostname, rawOutput, afero.NewOsFs()) + if rawOutput && utils.OutputFormat.Value == utils.OutputPretty { + utils.OutputFormat.Value = utils.OutputJson + } + return create.Run(cmd.Context(), flags.ProjectRef, customHostname, afero.NewOsFs()) }, } @@ -41,7 +45,10 @@ Expects your custom hostname to have a CNAME record to your Supabase project's s Short: "Get the current custom hostname config", Long: "Retrieve the custom hostname config for your project, as stored in the Supabase platform.", RunE: func(cmd *cobra.Command, args []string) error { - return get.Run(cmd.Context(), flags.ProjectRef, rawOutput, afero.NewOsFs()) + if rawOutput && utils.OutputFormat.Value == utils.OutputPretty { + utils.OutputFormat.Value = utils.OutputJson + } + return get.Run(cmd.Context(), flags.ProjectRef, afero.NewOsFs()) }, } @@ -49,7 +56,10 @@ Expects your custom hostname to have a CNAME record to your Supabase project's s Use: "reverify", Short: "Re-verify the custom hostname config for your project", RunE: func(cmd *cobra.Command, args []string) error { - return reverify.Run(cmd.Context(), flags.ProjectRef, rawOutput, afero.NewOsFs()) + if rawOutput && utils.OutputFormat.Value == utils.OutputPretty { + utils.OutputFormat.Value = utils.OutputJson + } + return reverify.Run(cmd.Context(), flags.ProjectRef, afero.NewOsFs()) }, } @@ -61,7 +71,10 @@ Expects your custom hostname to have a CNAME record to your Supabase project's s This reconfigures your Supabase project to respond to requests on your custom hostname. After the custom hostname is activated, your project's auth services will no longer function on the Supabase-provisioned subdomain.`, RunE: func(cmd *cobra.Command, args []string) error { - return activate.Run(cmd.Context(), flags.ProjectRef, rawOutput, afero.NewOsFs()) + if rawOutput && utils.OutputFormat.Value == utils.OutputPretty { + utils.OutputFormat.Value = utils.OutputJson + } + return activate.Run(cmd.Context(), flags.ProjectRef, afero.NewOsFs()) }, } @@ -69,6 +82,9 @@ After the custom hostname is activated, your project's auth services will no lon Use: "delete", Short: "Deletes the custom hostname config for your project", RunE: func(cmd *cobra.Command, args []string) error { + if rawOutput && utils.OutputFormat.Value == utils.OutputPretty { + utils.OutputFormat.Value = utils.OutputJson + } return delete.Run(cmd.Context(), flags.ProjectRef, afero.NewOsFs()) }, } @@ -78,12 +94,14 @@ func init() { persistentFlags := customHostnamesCmd.PersistentFlags() persistentFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") persistentFlags.BoolVar(&rawOutput, "include-raw-output", false, "Include raw output (useful for debugging).") - customHostnamesCreateCmd.Flags().StringVar(&customHostname, "custom-hostname", "", "The custom hostname to use for your Supabase project.") + cobra.CheckErr(persistentFlags.MarkDeprecated("include-raw-output", "use -o json instead")) + createFlags := customHostnamesCreateCmd.Flags() + createFlags.StringVar(&customHostname, "custom-hostname", "", "The custom hostname to use for your Supabase project.") + cobra.CheckErr(customHostnamesCreateCmd.MarkFlagRequired("custom-hostname")) customHostnamesCmd.AddCommand(customHostnamesGetCmd) customHostnamesCmd.AddCommand(customHostnamesCreateCmd) customHostnamesCmd.AddCommand(customHostnamesReverifyCmd) customHostnamesCmd.AddCommand(customHostnamesActivateCmd) customHostnamesCmd.AddCommand(customHostnamesDeleteCmd) - rootCmd.AddCommand(customHostnamesCmd) } diff --git a/cmd/encryption.go b/cmd/encryption.go index d3b654afcb..77b114ec50 100644 --- a/cmd/encryption.go +++ b/cmd/encryption.go @@ -1,8 +1,6 @@ package cmd import ( - "os" - "github.com/spf13/cobra" "github.com/supabase/cli/internal/encryption/get" "github.com/supabase/cli/internal/encryption/update" @@ -28,7 +26,7 @@ var ( Use: "update-root-key", Short: "Update root encryption key of a Supabase project", RunE: func(cmd *cobra.Command, args []string) error { - return update.Run(cmd.Context(), flags.ProjectRef, os.Stdin) + return update.Run(cmd.Context(), flags.ProjectRef) }, } ) diff --git a/cmd/gen.go b/cmd/gen.go index d15908a7b2..05f34cbea2 100644 --- a/cmd/gen.go +++ b/cmd/gen.go @@ -13,11 +13,11 @@ import ( "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/supabase/cli/internal/gen/bearerjwt" - "github.com/supabase/cli/internal/gen/keys" "github.com/supabase/cli/internal/gen/signingkeys" "github.com/supabase/cli/internal/gen/types" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/legacy/keys" "github.com/supabase/cli/pkg/config" ) diff --git a/cmd/login.go b/cmd/login.go index d1d4086fdf..64de8c77a9 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -42,7 +42,7 @@ var ( PostRunE: func(cmd *cobra.Command, args []string) error { if prof := viper.GetString("PROFILE"); viper.IsSet("PROFILE") { // Failure to save should block subsequent commands on CI - return utils.WriteFile(utils.ProfilePath, []byte(prof), afero.NewOsFs()) + return utils.SaveProfileName(prof, afero.NewOsFs()) } return nil }, diff --git a/cmd/projects.go b/cmd/projects.go index 93a75fb20b..2b4763c6d1 100644 --- a/cmd/projects.go +++ b/cmd/projects.go @@ -49,7 +49,6 @@ var ( string(api.V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedMemory), string(api.V1CreateProjectBodyDesiredInstanceSizeN4xlarge), string(api.V1CreateProjectBodyDesiredInstanceSizeN8xlarge), - string(api.V1CreateProjectBodyDesiredInstanceSizePico), string(api.V1CreateProjectBodyDesiredInstanceSizeSmall), string(api.V1CreateProjectBodyDesiredInstanceSizeXlarge), }, diff --git a/cmd/vanitySubdomains.go b/cmd/vanitySubdomains.go index dd3608c53e..44d87e8d7a 100644 --- a/cmd/vanitySubdomains.go +++ b/cmd/vanitySubdomains.go @@ -64,11 +64,12 @@ After the vanity subdomain is activated, your project's auth services will no lo func init() { vanityCmd.PersistentFlags().StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") vanityActivateCmd.Flags().StringVar(&desiredSubdomain, "desired-subdomain", "", "The desired vanity subdomain to use for your Supabase project.") + cobra.CheckErr(vanityActivateCmd.MarkFlagRequired("desired-subdomain")) vanityCheckCmd.Flags().StringVar(&desiredSubdomain, "desired-subdomain", "", "The desired vanity subdomain to use for your Supabase project.") + cobra.CheckErr(vanityCheckCmd.MarkFlagRequired("desired-subdomain")) vanityCmd.AddCommand(vanityGetCmd) vanityCmd.AddCommand(vanityCheckCmd) vanityCmd.AddCommand(vanityActivateCmd) vanityCmd.AddCommand(vanityDeleteCmd) - rootCmd.AddCommand(vanityCmd) } diff --git a/docs/supabase/db/dump.md b/docs/supabase/db/dump.md index 2ccfef091e..3d5c4fae54 100644 --- a/docs/supabase/db/dump.md +++ b/docs/supabase/db/dump.md @@ -7,3 +7,12 @@ Requires your local project to be linked to a remote database by running `supaba Runs `pg_dump` in a container with additional flags to exclude Supabase managed schemas. The ignored schemas include auth, storage, and those created by extensions. The default dump does not contain any data or custom roles. To dump those contents explicitly, specify either the `--data-only` and `--role-only` flag. + +### Note on Privilege Migration + +When restoring to a new project, tables inherit ALL privileges from default privileges in the target database. To preserve specific privileges from your dump, revoke defaults before restoring: + +```sql +-- Run BEFORE restoring your schema +ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated; +``` diff --git a/e2e-test.sh b/e2e-test.sh new file mode 100755 index 0000000000..80254f1be7 --- /dev/null +++ b/e2e-test.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -eou pipefail + +dotenv=$("${1:-supabase}" --workdir tests status -o env) +export $(echo "$dotenv" | xargs) + +for tc in ./tests/*.sh; do + echo "Running $tc" >&2 + exec "$tc" & +done + +wait +status="$?" + +echo "All tests have completed." >&2 +exit "$status" diff --git a/go.mod b/go.mod index d95d9660b3..dc28821eae 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,9 @@ 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.21.0 + github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/glamour v0.10.0 + github.com/charmbracelet/glamour v1.0.0 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/compose-spec/compose-go/v2 v2.9.1 github.com/containerd/errdefs v1.0.0 @@ -20,15 +20,16 @@ require ( github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/fsnotify/fsnotify v1.9.0 - github.com/getsentry/sentry-go v0.42.0 + github.com/getsentry/sentry-go v0.43.0 github.com/go-errors/errors v1.5.1 - github.com/go-git/go-git/v5 v5.16.4 + github.com/go-git/go-git/v5 v5.17.0 github.com/go-playground/validator/v10 v10.30.1 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/go-xmlfmt/xmlfmt v1.1.3 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/go-github/v62 v62.0.0 github.com/google/go-querystring v1.2.0 + github.com/google/jsonschema-go v0.4.2 github.com/google/uuid v1.6.0 github.com/h2non/gock v1.2.0 github.com/jackc/pgconn v1.14.3 @@ -40,8 +41,8 @@ require ( github.com/muesli/reflow v0.3.0 github.com/multigres/multigres v0.0.0-20260126223308-f5a52171bbc4 github.com/oapi-codegen/nullable v1.1.0 - github.com/olekukonko/tablewriter v1.1.3 - github.com/slack-go/slack v0.17.3 + github.com/olekukonko/tablewriter v1.1.4 + github.com/slack-go/slack v0.19.0 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -49,15 +50,15 @@ require ( github.com/stretchr/testify v1.11.1 github.com/stripe/pg-schema-diff v1.0.5 github.com/supabase/cli/pkg v1.0.0 - github.com/tidwall/jsonc v0.3.2 + github.com/tidwall/jsonc v0.3.3 github.com/withfig/autocomplete-tools/packages/cobra v1.2.0 github.com/zalando/go-keyring v0.2.6 - go.opentelemetry.io/otel v1.40.0 - golang.org/x/mod v0.32.0 - golang.org/x/net v0.49.0 - golang.org/x/oauth2 v0.34.0 - golang.org/x/term v0.39.0 - google.golang.org/grpc v1.78.0 + go.opentelemetry.io/otel v1.42.0 + golang.org/x/mod v0.34.0 + golang.org/x/net v0.52.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/term v0.41.0 + google.golang.org/grpc v1.79.2 gopkg.in/yaml.v3 v3.0.1 ) @@ -80,7 +81,7 @@ require ( github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/alecthomas/chroma/v2 v2.17.2 // indirect + github.com/alecthomas/chroma/v2 v2.20.0 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect github.com/alexkohler/nakedret/v2 v2.0.6 // indirect github.com/alexkohler/prealloc v1.0.0 // indirect @@ -121,18 +122,17 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charithe/durationcheck v0.0.10 // indirect - github.com/charmbracelet/colorprofile v0.3.1 // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.10.1 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect github.com/chavacava/garif v0.1.0 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect - github.com/clipperhouse/displaywidth v0.6.2 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.6.1 // indirect + github.com/clipperhouse/displaywidth v0.10.0 // indirect + github.com/clipperhouse/uax29/v2 v2.6.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/containerd/console v1.0.5 // indirect github.com/containerd/containerd/api v1.9.0 // indirect github.com/containerd/containerd/v2 v2.1.5 // indirect @@ -166,7 +166,7 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // 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.16.8 // indirect + github.com/ethereum/go-ethereum v1.17.0 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect @@ -181,7 +181,7 @@ require ( github.com/ghostiam/protogetter v0.3.15 // indirect github.com/go-critic/go-critic v0.13.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-billy/v5 v5.8.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -267,7 +267,7 @@ require ( 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 + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/manuelarte/funcorder v0.2.1 // indirect @@ -318,12 +318,12 @@ require ( github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.19.1 // indirect github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 // indirect - github.com/oapi-codegen/runtime v1.1.2 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect - github.com/olekukonko/errors v1.1.0 // indirect - github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect + github.com/olekukonko/errors v1.2.0 // indirect + github.com/olekukonko/ll v0.1.6 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect @@ -400,8 +400,8 @@ require ( github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect - github.com/yuin/goldmark v1.7.8 // indirect - github.com/yuin/goldmark-emoji v1.0.5 // indirect + github.com/yuin/goldmark v1.7.13 // indirect + github.com/yuin/goldmark-emoji v1.0.6 // indirect github.com/zclconf/go-cty v1.17.0 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.13.1 // indirect @@ -416,23 +416,23 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/sdk v1.39.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.47.0 // indirect + golang.org/x/crypto v0.49.0 // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.40.0 // indirect + golang.org/x/tools v0.42.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-20251222181119-0a764e51fe1b // indirect diff --git a/go.sum b/go.sum index baae9c767f..c18ac2d16b 100644 --- a/go.sum +++ b/go.sum @@ -50,12 +50,12 @@ github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpH github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI= -github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= +github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= +github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= +github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ= @@ -112,8 +112,8 @@ github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -161,28 +161,28 @@ 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.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= -github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= +github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= -github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= -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/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08= +github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo= 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.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.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= -github.com/charmbracelet/x/ansi v0.10.1/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/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= 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/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -190,16 +190,14 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= -github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= -github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= +github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= +github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= +github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= 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/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= @@ -319,8 +317,8 @@ github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FM 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.16.8 h1:LLLfkZWijhR5m6yrAXbdlTeXoqontH+Ga2f9igY7law= -github.com/ethereum/go-ethereum v1.16.8/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= +github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -347,8 +345,8 @@ github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCK github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 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.42.0 h1:eeFMACuZTbUQf90RE8dE4tXeSe4CZyfvR1MBL7RLEt8= -github.com/getsentry/sentry-go v0.42.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= +github.com/getsentry/sentry-go v0.43.0 h1:XbXLpFicpo8HmBDaInk7dum18G9KSLcjZiyUKS+hLW4= +github.com/getsentry/sentry-go v0.43.0/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0= 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= @@ -359,12 +357,12 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= -github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= +github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= -github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM= +github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -499,6 +497,8 @@ github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= +github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= @@ -698,8 +698,8 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -832,20 +832,20 @@ github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/ github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q= github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8= -github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= -github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= -github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= -github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM= -github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= -github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= -github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= +github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= +github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= +github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= +github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= +github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= @@ -1002,8 +1002,8 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= -github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g= -github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk= +github.com/slack-go/slack v0.19.0 h1:J8lL/nGTsIUX53HU8YxZeI3PDkA+sxZsFrI2Dew7h44= +github.com/slack-go/slack v0.19.0/go.mod h1:K81UmCivcYd/5Jmz8vLBfuyoZ3B4rQC2GHVXHteXiAE= github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= @@ -1066,8 +1066,8 @@ github.com/tetafro/godot v1.5.1 h1:PZnjCol4+FqaEzvZg5+O8IY2P3hfY9JzRBNPv1pEDS4= github.com/tetafro/godot v1.5.1/go.mod h1:cCdPtEndkmqqrhiCfkmxDodMQJ/f3L1BCNskCUZdTwk= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= -github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc= -github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE= +github.com/tidwall/jsonc v0.3.3 h1:RVQqL3xFfDkKKXIDsrBiVQiEpBtxoKbmMXONb2H/y2w= +github.com/tidwall/jsonc v0.3.3/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk= @@ -1088,8 +1088,8 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= @@ -1135,11 +1135,10 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= -github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= -github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= @@ -1165,8 +1164,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0. go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE= @@ -1177,14 +1176,14 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -1231,8 +1230,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= @@ -1253,8 +1252,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -1280,10 +1279,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1296,8 +1295,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1342,8 +1341,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1354,8 +1353,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1368,8 +1367,8 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1400,8 +1399,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= @@ -1419,8 +1418,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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/backups/list/list.go b/internal/backups/list/list.go index b4b16dd1c8..5df9ae252a 100644 --- a/internal/backups/list/list.go +++ b/internal/backups/list/list.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strings" "github.com/go-errors/errors" "github.com/supabase/cli/internal/utils" @@ -45,21 +46,20 @@ const ( ) func listLogicalBackups(resp api.V1BackupsResponse) error { - table := `REGION|BACKUP TYPE|STATUS|CREATED AT (UTC) + var table strings.Builder + table.WriteString(`REGION|BACKUP TYPE|STATUS|CREATED AT (UTC) |-|-|-|-| -` +`) for _, backup := range resp.Backups { backupType := BACKUP_LOGICAL if backup.IsPhysicalBackup { backupType = BACKUP_PHYSICAL } - table += fmt.Sprintf( - "|`%s`|`%s`|`%s`|`%s`|\n", + fmt.Fprintf(&table, "|`%s`|`%s`|`%s`|`%s`|\n", utils.FormatRegion(resp.Region), backupType, backup.Status, - utils.FormatTimestamp(backup.InsertedAt), - ) + utils.FormatTimestamp(backup.InsertedAt)) } - return utils.RenderTable(table) + return utils.RenderTable(table.String()) } diff --git a/internal/backups/list/list_test.go b/internal/backups/list/list_test.go new file mode 100644 index 0000000000..70285ec6b1 --- /dev/null +++ b/internal/backups/list/list_test.go @@ -0,0 +1,119 @@ +package list + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestListBackup(t *testing.T) { + // Setup valid project ref + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("lists PITR backup", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, ` + + REGION | WALG | PITR | EARLIEST TIMESTAMP | LATEST TIMESTAMP + ----------------------------|------|------|--------------------|------------------ + Southeast Asia (Singapore) | true | true | 0 | 0 + +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/database/backups"). + Reply(http.StatusOK). + JSON(api.V1BackupsResponse{ + Region: "ap-southeast-1", + WalgEnabled: true, + PitrEnabled: true, + }) + // Run test + err := Run(context.Background()) + assert.NoError(t, err) + }) + + t.Run("lists WALG backup", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, ` + + REGION | BACKUP TYPE | STATUS | CREATED AT (UTC) + ----------------------------|-------------|-----------|--------------------- + Southeast Asia (Singapore) | PHYSICAL | COMPLETED | 2026-02-08 16:44:07 + +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/database/backups"). + Reply(http.StatusOK). + JSON(api.V1BackupsResponse{ + Region: "ap-southeast-1", + Backups: []struct { + InsertedAt string `json:"inserted_at"` + IsPhysicalBackup bool `json:"is_physical_backup"` + Status api.V1BackupsResponseBackupsStatus `json:"status"` + }{{ + InsertedAt: "2026-02-08 16:44:07", + IsPhysicalBackup: true, + Status: api.V1BackupsResponseBackupsStatusCOMPLETED, + }}, + }) + // Run test + err := Run(context.Background()) + assert.NoError(t, err) + }) + + t.Run("encodes json output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputJson + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `{ + "backups": null, + "physical_backup_data": {}, + "pitr_enabled": false, + "region": "ap-southeast-1", + "walg_enabled": false +} +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/database/backups"). + Reply(http.StatusOK). + JSON(api.V1BackupsResponse{Region: "ap-southeast-1"}) + // Run test + err := Run(context.Background()) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/database/backups"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background()) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/database/backups"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background()) + assert.ErrorContains(t, err, "unexpected list backup status 503:") + }) +} diff --git a/internal/backups/restore/restore.go b/internal/backups/restore/restore.go index 8118f1bcde..6219039f92 100644 --- a/internal/backups/restore/restore.go +++ b/internal/backups/restore/restore.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "os" "github.com/go-errors/errors" "github.com/supabase/cli/internal/utils" @@ -13,12 +14,11 @@ import ( func Run(ctx context.Context, timestamp int64) error { body := api.V1RestorePitrBody{RecoveryTimeTargetUnix: timestamp} - resp, err := utils.GetSupabase().V1RestorePitrBackupWithResponse(ctx, flags.ProjectRef, body) - if err != nil { + if resp, err := utils.GetSupabase().V1RestorePitrBackupWithResponse(ctx, flags.ProjectRef, body); err != nil { return errors.Errorf("failed to restore backup: %w", err) } else if resp.StatusCode() != http.StatusCreated { return errors.Errorf("unexpected restore backup status %d: %s", resp.StatusCode(), string(resp.Body)) } - fmt.Println("Started PITR restore:", flags.ProjectRef) + fmt.Fprintln(os.Stderr, "Started PITR restore:", flags.ProjectRef) return nil } diff --git a/internal/backups/restore/restore_test.go b/internal/backups/restore/restore_test.go new file mode 100644 index 0000000000..e0ff3714e3 --- /dev/null +++ b/internal/backups/restore/restore_test.go @@ -0,0 +1,53 @@ +package restore + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" +) + +func TestRestoreBackup(t *testing.T) { + // Setup valid project ref + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("restores project to timestamp", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/database/backups/restore-pitr"). + Reply(http.StatusCreated) + // Run test + err := Run(context.Background(), 0) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/database/backups/restore-pitr"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), 0) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/database/backups/restore-pitr"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), 0) + assert.ErrorContains(t, err, "unexpected restore backup status 503:") + }) +} diff --git a/internal/bans/get/get.go b/internal/bans/get/get.go index 5b747325ca..c21558d709 100644 --- a/internal/bans/get/get.go +++ b/internal/bans/get/get.go @@ -3,8 +3,11 @@ package get import ( "context" "fmt" + "os" + "github.com/go-errors/errors" "github.com/spf13/afero" + "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" ) @@ -13,6 +16,18 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { if err != nil { return err } - fmt.Printf("DB banned IPs: %+v\n", ips) - return nil + fmt.Fprintln(os.Stderr, "DB banned IPs:") + switch utils.OutputFormat.Value { + case utils.OutputPretty: + utils.OutputFormat.Value = utils.OutputJson + case utils.OutputToml: + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, struct { + BannedIPs []string `toml:"banned_ips"` + }{ + BannedIPs: ips, + }) + case utils.OutputEnv: + return errors.New(utils.ErrEnvNotSupported) + } + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, ips) } diff --git a/internal/bans/get/get_test.go b/internal/bans/get/get_test.go new file mode 100644 index 0000000000..21abb64e54 --- /dev/null +++ b/internal/bans/get/get_test.go @@ -0,0 +1,86 @@ +package get + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestGetBans(t *testing.T) { + t.Run("list network bans", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, `[ + "192.168.0.1", + "192.168.0.2" +] +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/network-bans/retrieve"). + Reply(http.StatusCreated). + JSON(api.NetworkBanResponse{ + BannedIpv4Addresses: []string{ + "192.168.0.1", + "192.168.0.2", + }, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("outputs toml format", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `banned_ips = ["127.0.0.1"] +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/network-bans/retrieve"). + Reply(http.StatusCreated). + JSON(api.NetworkBanResponse{ + BannedIpv4Addresses: []string{"127.0.0.1"}, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/network-bans/retrieve"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on env format", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/network-bans/retrieve"). + Reply(http.StatusCreated). + JSON(api.NetworkBanResponse{ + BannedIpv4Addresses: []string{"127.0.0.1"}, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, utils.ErrEnvNotSupported) + }) +} diff --git a/internal/branches/create/create.go b/internal/branches/create/create.go index 6d5b9c7a15..5ee14aacd9 100644 --- a/internal/branches/create/create.go +++ b/internal/branches/create/create.go @@ -8,14 +8,13 @@ import ( "github.com/go-errors/errors" "github.com/spf13/afero" "github.com/supabase/cli/internal/branches/list" - "github.com/supabase/cli/internal/gen/keys" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" "github.com/supabase/cli/pkg/api" ) func Run(ctx context.Context, body api.CreateBranchBody, fsys afero.Fs) error { - gitBranch := keys.GetGitBranchOrDefault("", fsys) + gitBranch := utils.GetGitBranchOrDefault("", fsys) if len(body.BranchName) == 0 && len(gitBranch) > 0 { title := fmt.Sprintf("Do you want to create a branch named %s?", utils.Aqua(gitBranch)) if shouldCreate, err := utils.NewConsole().PromptYesNo(ctx, title, true); err != nil { diff --git a/internal/branches/delete/delete.go b/internal/branches/delete/delete.go index 9b37302471..d73762cedf 100644 --- a/internal/branches/delete/delete.go +++ b/internal/branches/delete/delete.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "os" "github.com/go-errors/errors" "github.com/supabase/cli/internal/branches/pause" @@ -21,10 +22,9 @@ func Run(ctx context.Context, branchId string, force *bool) error { }) if err != nil { return errors.Errorf("failed to delete preview branch: %w", err) + } else if resp.StatusCode() != http.StatusOK { + return errors.Errorf("unexpected delete branch status %d: %s", resp.StatusCode(), string(resp.Body)) } - if resp.StatusCode() != http.StatusOK { - return errors.New("Unexpected error deleting preview branch: " + string(resp.Body)) - } - fmt.Println("Deleted preview branch:", projectRef) + fmt.Fprintln(os.Stderr, "Deleted preview branch:", projectRef) return nil } diff --git a/internal/branches/delete/delete_test.go b/internal/branches/delete/delete_test.go new file mode 100644 index 0000000000..cf3dade79c --- /dev/null +++ b/internal/branches/delete/delete_test.go @@ -0,0 +1,64 @@ +package delete + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" +) + +func TestDeleteBranch(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("deletes existing branch", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("/v1/branches/" + flags.ProjectRef). + Reply(http.StatusOK) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on branch not found", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches/missing"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), "missing", nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("/v1/branches/" + flags.ProjectRef). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("/v1/branches/" + flags.ProjectRef). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected delete branch status 503:") + }) +} diff --git a/internal/branches/disable/disable.go b/internal/branches/disable/disable.go index d94c1e6c5a..9cbf38434e 100644 --- a/internal/branches/disable/disable.go +++ b/internal/branches/disable/disable.go @@ -17,7 +17,7 @@ func Run(ctx context.Context, fsys afero.Fs) error { return errors.Errorf("failed to disable preview branching: %w", err) } if resp.StatusCode() != http.StatusOK { - return errors.New("Unexpected error disabling preview branching: " + string(resp.Body)) + return errors.Errorf("unexpected disable branching status %d: %s", resp.StatusCode(), string(resp.Body)) } fmt.Println("Disabled preview branching for project:", flags.ProjectRef) return nil diff --git a/internal/branches/disable/disable_test.go b/internal/branches/disable/disable_test.go new file mode 100644 index 0000000000..2bc91cae0f --- /dev/null +++ b/internal/branches/disable/disable_test.go @@ -0,0 +1,52 @@ +package disable + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" +) + +func TestDisableBranching(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("disables branching", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("/v1/projects/" + flags.ProjectRef + "/branches"). + Reply(http.StatusOK) + // Run test + err := Run(context.Background(), nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("/v1/projects/" + flags.ProjectRef + "/branches"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("/v1/projects/" + flags.ProjectRef + "/branches"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), nil) + assert.ErrorContains(t, err, "unexpected disable branching status 503:") + }) +} diff --git a/internal/branches/get/get_test.go b/internal/branches/get/get_test.go new file mode 100644 index 0000000000..7afd11cce7 --- /dev/null +++ b/internal/branches/get/get_test.go @@ -0,0 +1,204 @@ +package get + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/oapi-codegen/nullable" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" + "github.com/supabase/cli/pkg/cast" +) + +func TestGetBranch(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("fetches branch details", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, ` + + HOST | PORT | USER | PASSWORD | JWT SECRET | POSTGRES VERSION | STATUS + -----------|------|--------|----------|------------|------------------------------|---------------- + 127.0.0.1 | 5432 | ****** | ****** | ****** | supabase-postgres-17.4.1.074 | ACTIVE_HEALTHY + +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/branches/" + flags.ProjectRef). + Reply(http.StatusOK). + JSON(api.BranchDetailResponse{ + DbHost: "127.0.0.1", + DbPort: 5432, + PostgresVersion: "supabase-postgres-17.4.1.074", + Status: api.BranchDetailResponseStatusACTIVEHEALTHY, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/branches/" + flags.ProjectRef). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/branches/" + flags.ProjectRef). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected get branch status 503:") + }) +} + +func TestTomlOutput(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + // Setup output format + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + + t.Run("encodes toml format", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, fmt.Sprintf(`POSTGRES_URL = "postgresql://postgres:postgres@127.0.0.1:6543/postgres?connect_timeout=10" +POSTGRES_URL_NON_POOLING = "postgresql://postgres:postgres@127.0.0.1:5432/postgres?connect_timeout=10" +SUPABASE_ANON_KEY = "anon-key" +SUPABASE_JWT_SECRET = "secret-key" +SUPABASE_SERVICE_ROLE_KEY = "service-role-key" +SUPABASE_URL = "https://%s." +`, flags.ProjectRef))) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/branches/" + flags.ProjectRef). + Reply(http.StatusOK). + JSON(api.BranchDetailResponse{ + DbHost: "127.0.0.1", + DbPort: 5432, + DbUser: cast.Ptr("postgres"), + DbPass: cast.Ptr("postgres"), + JwtSecret: cast.Ptr("secret-key"), + Ref: flags.ProjectRef, + }) + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/api-keys"). + Reply(http.StatusOK). + JSON([]api.ApiKeyResponse{{ + Name: "anon", + ApiKey: nullable.NewNullableWithValue("anon-key"), + }, { + Name: "service_role", + ApiKey: nullable.NewNullableWithValue("service-role-key"), + }}) + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/config/database/pooler"). + Reply(http.StatusOK). + JSON([]api.SupavisorConfigResponse{{ + ConnectionString: "postgres://postgres:postgres@127.0.0.1:6543/postgres", + DatabaseType: api.SupavisorConfigResponseDatabaseTypePRIMARY, + PoolMode: api.SupavisorConfigResponsePoolModeTransaction, + }}) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/branches/" + flags.ProjectRef). + Reply(http.StatusOK). + JSON(api.BranchDetailResponse{ + Ref: flags.ProjectRef, + }) + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/api-keys"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on database not found", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/branches/" + flags.ProjectRef). + Reply(http.StatusOK). + JSON(api.BranchDetailResponse{ + Ref: flags.ProjectRef, + }) + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/api-keys"). + Reply(http.StatusOK). + JSON([]api.ApiKeyResponse{}) + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/config/database/pooler"). + Reply(http.StatusOK). + JSON([]api.SupavisorConfigResponse{}) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, utils.ErrPrimaryNotFound) + }) +} + +func TestBranchDetail(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("get branch by name", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches/main"). + Reply(http.StatusOK). + JSON(api.BranchResponse{ProjectRef: flags.ProjectRef}) + gock.New(utils.DefaultApiHost). + Get("/v1/branches/" + flags.ProjectRef). + Reply(http.StatusOK). + JSON(api.BranchDetailResponse{}) + // Run test + _, err := getBranchDetail(context.Background(), "main") + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches/main"). + ReplyError(errNetwork) + // Run test + _, err := getBranchDetail(context.Background(), "main") + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on branch not found", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches/missing"). + Reply(http.StatusNotFound) + // Run test + _, err := getBranchDetail(context.Background(), "missing") + assert.ErrorContains(t, err, "unexpected find branch status 404:") + }) +} diff --git a/internal/branches/list/list.go b/internal/branches/list/list.go index 12dad2c0ba..1f9b545d23 100644 --- a/internal/branches/list/list.go +++ b/internal/branches/list/list.go @@ -37,16 +37,16 @@ func Run(ctx context.Context, fsys afero.Fs) error { } func ToMarkdown(branches []api.BranchResponse) string { - table := `|ID|NAME|DEFAULT|GIT BRANCH|WITH DATA|STATUS|CREATED AT (UTC)|UPDATED AT (UTC)| + var table strings.Builder + table.WriteString(`|ID|NAME|DEFAULT|GIT BRANCH|WITH DATA|STATUS|CREATED AT (UTC)|UPDATED AT (UTC)| |-|-|-|-|-|-|-|-| -` +`) for _, branch := range branches { gitBranch := " " if branch.GitBranch != nil { gitBranch = *branch.GitBranch } - table += fmt.Sprintf( - "|`%s`|`%s`|`%t`|`%s`|`%t`|`%s`|`%s`|`%s`|\n", + fmt.Fprintf(&table, "|`%s`|`%s`|`%t`|`%s`|`%t`|`%s`|`%s`|`%s`|\n", branch.ProjectRef, strings.ReplaceAll(branch.Name, "|", "\\|"), branch.IsDefault, @@ -54,10 +54,9 @@ func ToMarkdown(branches []api.BranchResponse) string { branch.WithData, branch.Status, utils.FormatTime(branch.CreatedAt), - utils.FormatTime(branch.UpdatedAt), - ) + utils.FormatTime(branch.UpdatedAt)) } - return table + return table.String() } type BranchFilter func(api.BranchResponse) bool diff --git a/internal/branches/list/list_test.go b/internal/branches/list/list_test.go new file mode 100644 index 0000000000..e7a111fa90 --- /dev/null +++ b/internal/branches/list/list_test.go @@ -0,0 +1,143 @@ +package list + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" + "github.com/supabase/cli/pkg/cast" +) + +func TestListBranches(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("lists all branches", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, ` + + ID | NAME | DEFAULT | GIT BRANCH | WITH DATA | STATUS | CREATED AT (UTC) | UPDATED AT (UTC) + ---------------------|---------|---------|------------|-----------|------------------|---------------------|--------------------- + staging-project-ref | Staging | false | develop | true | CREATING_PROJECT | 2026-01-02 03:04:05 | 2026-01-03 03:04:05 + +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches"). + Reply(http.StatusOK). + JSON([]api.BranchResponse{{ + GitBranch: cast.Ptr("develop"), + ProjectRef: "staging-project-ref", + Name: "Staging", + Persistent: true, + WithData: true, + Status: api.BranchResponseStatusCREATINGPROJECT, + CreatedAt: time.Date(2026, 01, 02, 03, 04, 05, 0, time.UTC), + UpdatedAt: time.Date(2026, 01, 03, 03, 04, 05, 0, time.UTC), + }}) + // Run test + err := Run(context.Background(), nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml format", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `[[branches]] + CreatedAt = 0001-01-01T00:00:00Z + Id = "00000000-0000-0000-0000-000000000000" + IsDefault = true + Name = "Production" + ParentProjectRef = "production-project-ref" + Persistent = false + ProjectRef = "production-project-ref" + Status = "FUNCTIONS_DEPLOYED" + UpdatedAt = 0001-01-01T00:00:00Z + WithData = false +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches"). + Reply(http.StatusOK). + JSON([]api.BranchResponse{{ + Name: "Production", + IsDefault: true, + ParentProjectRef: "production-project-ref", + ProjectRef: "production-project-ref", + Status: api.BranchResponseStatusFUNCTIONSDEPLOYED, + }}) + // Run test + err := Run(context.Background(), nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on env format", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches"). + Reply(http.StatusOK). + JSON([]api.BranchResponse{}) + // Run test + err := Run(context.Background(), nil) + assert.ErrorIs(t, err, utils.ErrEnvNotSupported) + }) +} + +func TestFilterBranch(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("filter branch by name", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches"). + Reply(http.StatusOK). + JSON([]api.BranchResponse{{ + Name: "Production", + IsDefault: true, + }, { + Name: "Staging", + Persistent: true, + }}) + // Run test + result, err := ListBranch(context.Background(), flags.ProjectRef, FilterByName("Production")) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.True(t, result[0].IsDefault) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches"). + Reply(http.StatusServiceUnavailable) + // Run test + _, err := ListBranch(context.Background(), flags.ProjectRef, FilterByName("Production")) + assert.ErrorContains(t, err, "unexpected list branch status 503:") + }) +} diff --git a/internal/branches/pause/pause_test.go b/internal/branches/pause/pause_test.go new file mode 100644 index 0000000000..20c442be9f --- /dev/null +++ b/internal/branches/pause/pause_test.go @@ -0,0 +1,127 @@ +package pause + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestPauseBranch(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("pauses a branch", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/pause"). + Reply(http.StatusOK) + // Run test + err := Run(context.Background(), flags.ProjectRef) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/pause"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/pause"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef) + assert.ErrorContains(t, err, "unexpected pause branch status 503:") + }) +} + +func TestBranchProjectRef(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("get by name", func(t *testing.T) { + branchRef := apitest.RandomProjectRef() + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches/develop"). + Reply(http.StatusOK). + JSON(api.BranchResponse{ProjectRef: branchRef}) + // run test + ref, err := GetBranchProjectRef(context.Background(), "develop") + assert.NoError(t, err) + assert.Equal(t, branchRef, ref) + }) + + t.Run("throws error on missing branch", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches/missing"). + Reply(http.StatusNotFound) + // Run test + _, err := GetBranchProjectRef(context.Background(), "missing") + assert.ErrorContains(t, err, "unexpected find branch status 404:") + }) + + t.Run("get by id", func(t *testing.T) { + brancRef := apitest.RandomProjectRef() + branchId, err := uuid.NewUUID() + require.NoError(t, err) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/branches/" + branchId.String()). + Reply(http.StatusOK). + JSON(api.BranchDetailResponse{Ref: brancRef}) + // Run test + ref, err := GetBranchProjectRef(context.Background(), branchId.String()) + assert.NoError(t, err) + assert.Equal(t, brancRef, ref) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + branchId, err := uuid.NewUUID() + require.NoError(t, err) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/branches/" + branchId.String()). + ReplyError(errNetwork) + // Run test + _, err = GetBranchProjectRef(context.Background(), branchId.String()) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + branchId, err := uuid.NewUUID() + require.NoError(t, err) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/branches/" + branchId.String()). + Reply(http.StatusServiceUnavailable) + // Run test + _, err = GetBranchProjectRef(context.Background(), branchId.String()) + assert.ErrorContains(t, err, "unexpected get branch status 503:") + }) +} diff --git a/internal/branches/unpause/unpause_test.go b/internal/branches/unpause/unpause_test.go new file mode 100644 index 0000000000..35851153e2 --- /dev/null +++ b/internal/branches/unpause/unpause_test.go @@ -0,0 +1,64 @@ +package unpause + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" +) + +func TestUnpauseBranch(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("unpause a branch", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/restore"). + Reply(http.StatusOK) + // Run test + err := Run(context.Background(), flags.ProjectRef) + assert.NoError(t, err) + }) + + t.Run("throws error on missing branch", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches/missing"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), "missing") + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/restore"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/restore"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef) + assert.ErrorContains(t, err, "unexpected unpause branch status 503:") + }) +} diff --git a/internal/branches/update/update.go b/internal/branches/update/update.go index 20ec6f82d0..a467ae1d2a 100644 --- a/internal/branches/update/update.go +++ b/internal/branches/update/update.go @@ -24,7 +24,7 @@ func Run(ctx context.Context, branchId string, body api.UpdateBranchBody, fsys a } else if resp.JSON200 == nil { return errors.Errorf("unexpected update branch status %d: %s", resp.StatusCode(), string(resp.Body)) } - fmt.Println("Updated preview branch:") + fmt.Fprintln(os.Stderr, "Updated preview branch:") if utils.OutputFormat.Value == utils.OutputPretty { table := list.ToMarkdown([]api.BranchResponse{*resp.JSON200}) return utils.RenderTable(table) diff --git a/internal/branches/update/update_test.go b/internal/branches/update/update_test.go new file mode 100644 index 0000000000..18382e94e0 --- /dev/null +++ b/internal/branches/update/update_test.go @@ -0,0 +1,109 @@ +package update + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestUpdateBranch(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("update branch attributes", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, ` + + ID | NAME | DEFAULT | GIT BRANCH | WITH DATA | STATUS | CREATED AT (UTC) | UPDATED AT (UTC) + --------------------|------|---------|------------|-----------|------------------|---------------------|--------------------- + branch-project-ref | Dev | false | | false | CREATING_PROJECT | 0001-01-01 00:00:00 | 0001-01-01 00:00:00 + +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Patch("/v1/branches/" + flags.ProjectRef). + Reply(http.StatusOK). + JSON(api.BranchResponse{ + Name: "Dev", + ProjectRef: "branch-project-ref", + Status: api.BranchResponseStatusCREATINGPROJECT, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, api.UpdateBranchBody{}, nil) + assert.NoError(t, err) + }) + + t.Run("encodes json format", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputJson + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `{ + "created_at": "0001-01-01T00:00:00Z", + "id": "00000000-0000-0000-0000-000000000000", + "is_default": false, + "name": "Dev", + "parent_project_ref": "", + "persistent": false, + "project_ref": "branch-project-ref", + "status": "CREATING_PROJECT", + "updated_at": "0001-01-01T00:00:00Z", + "with_data": false +} +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Patch("/v1/branches/" + flags.ProjectRef). + Reply(http.StatusOK). + JSON(api.BranchResponse{ + Name: "Dev", + ProjectRef: "branch-project-ref", + Status: api.BranchResponseStatusCREATINGPROJECT, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, api.UpdateBranchBody{}, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on missing branch", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/branches/missing"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), "missing", api.UpdateBranchBody{}, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Patch("/v1/branches/" + flags.ProjectRef). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, api.UpdateBranchBody{}, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Patch("/v1/branches/" + flags.ProjectRef). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, api.UpdateBranchBody{}, nil) + assert.ErrorContains(t, err, "unexpected update branch status 503:") + }) +} diff --git a/internal/config/push/push_test.go b/internal/config/push/push_test.go index 5af8fb273f..687ea5d052 100644 --- a/internal/config/push/push_test.go +++ b/internal/config/push/push_test.go @@ -11,14 +11,53 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/pkg/api" ) func TestPushConfig(t *testing.T) { project := apitest.RandomProjectRef() - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + + t.Run("pushes local config", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(fstest.MockStdin(t, "y")) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup in-memory fs + fsys := afero.NewMemMapFs() + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/"+project+"/billing/addons"). + Reply(http.StatusOK). + SetHeader("Content-Type", "application/json"). + BodyString(`{ + "available_addons":[{ + "name": "GraphQL", + "type": "api", + "variants": [{ + "id": "api_graphql", + "name": "GraphQL", + "price": { + "amount": 0.1027, + "description": "$75/month, then $10/month", + "interval": "hourly", + "type": "usage" + } + }] + }] + }`) + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/postgrest"). + Reply(http.StatusOK). + JSON(api.V1PostgrestConfigResponse{}) + gock.New(utils.DefaultApiHost). + Patch("/v1/projects/" + project + "/postgrest"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), project, fsys) + // Check error + assert.ErrorIs(t, err, errNetwork) + }) t.Run("throws error on malformed config", func(t *testing.T) { // Setup in-memory fs @@ -31,10 +70,10 @@ func TestPushConfig(t *testing.T) { }) t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup mock api - defer gock.OffAll() gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/billing/addons"). Reply(http.StatusServiceUnavailable) @@ -47,13 +86,10 @@ func TestPushConfig(t *testing.T) { func TestCostMatrix(t *testing.T) { project := apitest.RandomProjectRef() - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) t.Run("fetches cost matrix", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) // Setup mock api - defer gock.OffAll() gock.New(utils.DefaultApiHost). Get("/v1/projects/"+project+"/billing/addons"). Reply(http.StatusOK). @@ -66,10 +102,10 @@ func TestCostMatrix(t *testing.T) { "id": "auth_mfa_phone_default", "name": "Advanced MFA - Phone", "price": { - "amount": 0.1027, - "description": "$75/month, then $10/month", - "interval": "hourly", - "type": "usage" + "amount": 0.1027, + "description": "$75/month, then $10/month", + "interval": "hourly", + "type": "usage" } }] }, { @@ -79,10 +115,10 @@ func TestCostMatrix(t *testing.T) { "id": "auth_mfa_web_authn_default", "name": "Advanced MFA - WebAuthn", "price": { - "amount": 0.1027, - "description": "$75/month, then $10/month", - "interval": "hourly", - "type": "usage" + "amount": 0.1027, + "description": "$75/month, then $10/month", + "interval": "hourly", + "type": "usage" } }] }] @@ -100,8 +136,8 @@ func TestCostMatrix(t *testing.T) { t.Run("throws error on network error", func(t *testing.T) { errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) // Setup mock api - defer gock.OffAll() gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/billing/addons"). ReplyError(errNetwork) @@ -111,4 +147,17 @@ func TestCostMatrix(t *testing.T) { assert.ErrorIs(t, err, errNetwork) assert.Nil(t, cost) }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/billing/addons"). + Reply(http.StatusServiceUnavailable) + // Run test + cost, err := getCostMatrix(context.Background(), project) + // Check error + assert.ErrorContains(t, err, "unexpected list addons status 503:") + assert.Nil(t, cost) + }) } diff --git a/internal/db/diff/diff.go b/internal/db/diff/diff.go index 18d3420949..3f7854fc7a 100644 --- a/internal/db/diff/diff.go +++ b/internal/db/diff/diff.go @@ -22,7 +22,7 @@ import ( "github.com/jackc/pgx/v4" "github.com/spf13/afero" "github.com/supabase/cli/internal/db/start" - "github.com/supabase/cli/internal/gen/keys" + "github.com/supabase/cli/internal/migration/new" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/migration" "github.com/supabase/cli/pkg/parser" @@ -35,7 +35,7 @@ func Run(ctx context.Context, schema []string, file string, config pgconn.Config if err != nil { return err } - branch := keys.GetGitBranch(fsys) + branch := utils.GetGitBranch(fsys) fmt.Fprintln(os.Stderr, "Finished "+utils.Aqua("supabase db diff")+" on branch "+utils.Aqua(branch)+".\n") if err := SaveDiff(out, file, fsys); err != nil { return err @@ -48,6 +48,24 @@ func Run(ctx context.Context, schema []string, file string, config pgconn.Config return nil } +var warnDiff = `WARNING: The diff tool is not foolproof, so you may need to manually rearrange and modify the generated migration. +Run ` + utils.Aqua("supabase db reset") + ` to verify that the new migration does not generate errors.` + +func SaveDiff(out, file string, fsys afero.Fs) error { + if len(out) < 2 { + fmt.Fprintln(os.Stderr, "No schema changes found") + } else if len(file) > 0 { + path := new.GetMigrationPath(utils.GetCurrentTimestamp(), file) + if err := utils.WriteFile(path, []byte(out), fsys); err != nil { + return err + } + fmt.Fprintln(os.Stderr, warnDiff) + } else { + fmt.Println(out) + } + return nil +} + func loadDeclaredSchemas(fsys afero.Fs) ([]string, error) { if schemas := utils.Config.Db.Migrations.SchemaPaths; len(schemas) > 0 { return schemas.Files(afero.NewIOFS(fsys)) diff --git a/internal/db/diff/pgdelta.go b/internal/db/diff/pgdelta.go index 12153068f6..0403dd91f9 100644 --- a/internal/db/diff/pgdelta.go +++ b/internal/db/diff/pgdelta.go @@ -23,12 +23,15 @@ var pgDeltaScript string func DiffPgDelta(ctx context.Context, source, target pgconn.Config, schema []string, options ...func(*pgx.ConnConfig)) (string, error) { env := []string{ "SOURCE=" + utils.ToPostgresURL(source), - "TARGET=" + utils.ToPostgresURL(target), } if ca, err := types.GetRootCA(ctx, utils.ToPostgresURL(target), options...); err != nil { return "", err } else if len(ca) > 0 { - env = append(env, "PGDELTA_TARGET_SSLROOTCERT="+ca) + target.RuntimeParams["sslmode"] = "require" + env = append(env, + "TARGET="+utils.ToPostgresURL(target), + "PGDELTA_TARGET_SSLROOTCERT="+ca, + ) } if len(schema) > 0 { env = append(env, "INCLUDED_SCHEMAS="+strings.Join(schema, ",")) diff --git a/internal/db/diff/templates/pgdelta.ts b/internal/db/diff/templates/pgdelta.ts index cfdac0aa42..a8bbd65b4c 100644 --- a/internal/db/diff/templates/pgdelta.ts +++ b/internal/db/diff/templates/pgdelta.ts @@ -1,23 +1,17 @@ -import { createPlan } from "npm:@supabase/pg-delta@1.0.0-alpha.2"; -import { supabase } from "npm:@supabase/pg-delta@1.0.0-alpha.2/integrations/supabase"; +import { createPlan } from "npm:@supabase/pg-delta@1.0.0-alpha.3"; +import { supabase } from "npm:@supabase/pg-delta@1.0.0-alpha.3/integrations/supabase"; const source = Deno.env.get("SOURCE"); const target = Deno.env.get("TARGET"); +const opts = { ...supabase, role: "postgres" }; const includedSchemas = Deno.env.get("INCLUDED_SCHEMAS"); if (includedSchemas) { - supabase.filter = { schema: includedSchemas.split(",") }; + opts.filter = { schema: includedSchemas.split(",") }; } -supabase.role = "postgres"; -try { - const result = await createPlan(source, target, supabase); - const statements = result?.plan.statements ?? []; - for (const sql of statements) { - console.log(`${sql};`); - } -} catch (e) { - console.error(e); - // Force close event loop - throw new Error(""); +const result = await createPlan(source, target, opts); +const statements = result?.plan.statements ?? []; +for (const sql of statements) { + console.log(`${sql};`); } diff --git a/internal/db/reset/reset.go b/internal/db/reset/reset.go index 21df9f648f..d86f344975 100644 --- a/internal/db/reset/reset.go +++ b/internal/db/reset/reset.go @@ -21,7 +21,6 @@ import ( "github.com/jackc/pgx/v4" "github.com/spf13/afero" "github.com/supabase/cli/internal/db/start" - "github.com/supabase/cli/internal/gen/keys" "github.com/supabase/cli/internal/migration/apply" "github.com/supabase/cli/internal/migration/down" "github.com/supabase/cli/internal/migration/list" @@ -73,7 +72,7 @@ func Run(ctx context.Context, version string, last uint, config pgconn.Config, f return err } } - branch := keys.GetGitBranch(fsys) + branch := utils.GetGitBranch(fsys) fmt.Fprintln(os.Stderr, "Finished "+utils.Aqua("supabase db reset")+" on branch "+utils.Aqua(branch)+".") return nil } diff --git a/internal/db/test/test.go b/internal/db/test/test.go index dadb447367..2852ee9432 100644 --- a/internal/db/test/test.go +++ b/internal/db/test/test.go @@ -5,6 +5,7 @@ import ( _ "embed" "fmt" "os" + "path" "path/filepath" "github.com/docker/docker/api/types/container" @@ -35,6 +36,7 @@ func Run(ctx context.Context, testFiles []string, config pgconn.Config, fsys afe } binds := make([]string, len(testFiles)) cmd := []string{"pg_prove", "--ext", ".pg", "--ext", ".sql", "-r"} + var workingDir string for i, fp := range testFiles { if !filepath.IsAbs(fp) { fp = filepath.Join(utils.CurrentDirAbs, fp) @@ -42,6 +44,12 @@ func Run(ctx context.Context, testFiles []string, config pgconn.Config, fsys afe dockerPath := utils.ToDockerPath(fp) cmd = append(cmd, dockerPath) binds[i] = fmt.Sprintf("%s:%s:ro", fp, dockerPath) + if workingDir == "" { + workingDir = dockerPath + if path.Ext(dockerPath) != "" { + workingDir = path.Dir(dockerPath) + } + } } if viper.GetBool("DEBUG") { cmd = append(cmd, "--verbose") @@ -89,7 +97,8 @@ func Run(ctx context.Context, testFiles []string, config pgconn.Config, fsys afe "PGPASSWORD=" + config.Password, "PGDATABASE=" + config.Database, }, - Cmd: cmd, + Cmd: cmd, + WorkingDir: workingDir, }, hostConfig, network.NetworkingConfig{}, diff --git a/internal/encryption/get/get.go b/internal/encryption/get/get.go index 93c6986352..a48e21c8bc 100644 --- a/internal/encryption/get/get.go +++ b/internal/encryption/get/get.go @@ -12,12 +12,9 @@ func Run(ctx context.Context, projectRef string) error { resp, err := utils.GetSupabase().V1GetPgsodiumConfigWithResponse(ctx, projectRef) if err != nil { return errors.Errorf("failed to retrieve pgsodium config: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected get pgsodium config status %d: %s", resp.StatusCode(), string(resp.Body)) } - - if resp.JSON200 == nil { - return errors.New("Unexpected error retrieving project root key: " + string(resp.Body)) - } - fmt.Println(resp.JSON200.RootKey) return nil } diff --git a/internal/encryption/get/get_test.go b/internal/encryption/get/get_test.go index 8f280189be..42c16f0cef 100644 --- a/internal/encryption/get/get_test.go +++ b/internal/encryption/get/get_test.go @@ -5,6 +5,7 @@ import ( "net/http" "testing" + "github.com/go-errors/errors" "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/supabase/cli/internal/testing/apitest" @@ -13,40 +14,40 @@ import ( ) func TestGetRootKey(t *testing.T) { + project := apitest.RandomProjectRef() + t.Run("fetches project encryption key", func(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)) - // Flush pending mocks after test execution - defer gock.OffAll() + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/pgsodium"). Reply(http.StatusOK). JSON(api.PgsodiumConfigResponse{RootKey: "test-key"}) // Run test err := Run(context.Background(), project) - // Check error assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) }) - t.Run("throws on invalid credentials", func(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)) - // Flush pending mocks after test execution - defer gock.OffAll() + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/pgsodium"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), project) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/pgsodium"). - Reply(http.StatusForbidden) + Reply(http.StatusServiceUnavailable) // Run test err := Run(context.Background(), project) - // Check error - assert.ErrorContains(t, err, "Unexpected error retrieving project root key:") - assert.Empty(t, apitest.ListUnmatchedRequests()) + assert.ErrorContains(t, err, "unexpected get pgsodium config status 503:") }) } diff --git a/internal/encryption/update/update.go b/internal/encryption/update/update.go index ed576823ff..ca4b483f33 100644 --- a/internal/encryption/update/update.go +++ b/internal/encryption/update/update.go @@ -12,20 +12,17 @@ import ( "github.com/supabase/cli/pkg/api" ) -func Run(ctx context.Context, projectRef string, stdin *os.File) error { +func Run(ctx context.Context, projectRef string) error { fmt.Fprintf(os.Stderr, "Enter a new root key: ") - input := credentials.PromptMasked(stdin) + input := credentials.PromptMasked(os.Stdin) resp, err := utils.GetSupabase().V1UpdatePgsodiumConfigWithResponse(ctx, projectRef, api.UpdatePgsodiumConfigBody{ RootKey: strings.TrimSpace(input), }) if err != nil { return errors.Errorf("failed to update pgsodium config: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected update pgsodium config status %d: %s", resp.StatusCode(), string(resp.Body)) } - - if resp.JSON200 == nil { - return errors.New("Unexpected error updating project root key: " + string(resp.Body)) - } - - fmt.Println("Finished " + utils.Aqua("supabase root-key update") + ".") + fmt.Fprintln(os.Stderr, "Finished "+utils.Aqua("supabase root-key update")+".") return nil } diff --git a/internal/encryption/update/update_test.go b/internal/encryption/update/update_test.go index 8a6f609300..f15a107092 100644 --- a/internal/encryption/update/update_test.go +++ b/internal/encryption/update/update_test.go @@ -2,60 +2,55 @@ package update import ( "context" + "errors" "net/http" - "os" "testing" "github.com/h2non/gock" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/api" ) func TestUpdateRootKey(t *testing.T) { + project := apitest.RandomProjectRef() + t.Run("updates project encryption key", func(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)) - // Setup root key - r, w, err := os.Pipe() - require.NoError(t, err) - _, err = w.WriteString("test-key") - require.NoError(t, err) - require.NoError(t, w.Close()) - // Flush pending mocks after test execution - defer gock.OffAll() + t.Cleanup(fstest.MockStdin(t, "test-key")) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api gock.New(utils.DefaultApiHost). Put("/v1/projects/" + project + "/pgsodium"). JSON(api.UpdatePgsodiumConfigBody{RootKey: "test-key"}). Reply(http.StatusOK). JSON(api.PgsodiumConfigResponse{RootKey: "test-key"}) // Run test - err = Run(context.Background(), project, r) - // Check error + err := Run(context.Background(), project) assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) }) - t.Run("throws on invalid credentials", func(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)) - // Flush pending mocks after test execution - defer gock.OffAll() + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + project + "/pgsodium"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), project) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api gock.New(utils.DefaultApiHost). Put("/v1/projects/" + project + "/pgsodium"). - Reply(http.StatusForbidden) + Reply(http.StatusServiceUnavailable) // Run test - err := Run(context.Background(), project, nil) - // Check error - assert.ErrorContains(t, err, "Unexpected error updating project root key:") - assert.Empty(t, apitest.ListUnmatchedRequests()) + err := Run(context.Background(), project) + assert.ErrorContains(t, err, "unexpected update pgsodium config status 503:") }) } diff --git a/internal/functions/list/list.go b/internal/functions/list/list.go index c376a22a9a..ce4a59e216 100644 --- a/internal/functions/list/list.go +++ b/internal/functions/list/list.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strings" "time" "github.com/go-errors/errors" @@ -22,13 +23,13 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { switch utils.OutputFormat.Value { case utils.OutputPretty: - table := `|ID|NAME|SLUG|STATUS|VERSION|UPDATED_AT (UTC)| + var table strings.Builder + table.WriteString(`|ID|NAME|SLUG|STATUS|VERSION|UPDATED_AT (UTC)| |-|-|-|-|-|-| -` +`) for _, function := range *resp.JSON200 { t := time.UnixMilli(function.UpdatedAt) - table += fmt.Sprintf( - "|`%s`|`%s`|`%s`|`%s`|`%d`|`%s`|\n", + fmt.Fprintf(&table, "|`%s`|`%s`|`%s`|`%s`|`%d`|`%s`|\n", function.Id, function.Name, function.Slug, @@ -37,7 +38,7 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { t.UTC().Format("2006-01-02 15:04:05"), ) } - return utils.RenderTable(table) + return utils.RenderTable(table.String()) case utils.OutputToml: return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, struct { Functions []api.FunctionResponse `toml:"functions"` diff --git a/internal/functions/list/list_test.go b/internal/functions/list/list_test.go index 7b6197ff51..0d4a6ce246 100644 --- a/internal/functions/list/list_test.go +++ b/internal/functions/list/list_test.go @@ -10,31 +10,23 @@ import ( "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/api" + "github.com/supabase/cli/pkg/cast" ) 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) { + t.Cleanup(apitest.MockPlatformAPI(t)) // Setup in-memory fs fsys := afero.NewMemMapFs() - // Flush pending mocks after test execution - defer gock.OffAll() - - testEntrypointPath := "test-entrypoint-path" - testImportMapPath := "test-import-map-path" - testImportMap := false - testVerifyJwt := true - + // Setup mock api gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/functions"). - Reply(200). + Reply(http.StatusOK). JSON([]api.FunctionResponse{{ Id: "test-id", Name: "Test Function", @@ -43,44 +35,88 @@ func TestFunctionsListCommand(t *testing.T) { UpdatedAt: 1687423025152.000000, CreatedAt: 1687423025152.000000, Version: 1.000000, - VerifyJwt: &testVerifyJwt, - EntrypointPath: &testEntrypointPath, - ImportMap: &testImportMap, - ImportMapPath: &testImportMapPath, + VerifyJwt: cast.Ptr(true), + EntrypointPath: cast.Ptr("test-entrypoint-path"), + ImportMap: cast.Ptr(false), + ImportMapPath: cast.Ptr("test-import-map-path"), }}) // Run test err := Run(context.Background(), project, fsys) // Check error assert.NoError(t, err) - assert.Empty(t, apitest.ListUnmatchedRequests()) }) - 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() + t.Run("encodes toml format", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `functions = [] +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/functions"). - Reply(http.StatusServiceUnavailable) + Reply(http.StatusOK). + JSON([]api.FunctionResponse{}) // Run test - err := Run(context.Background(), project, fsys) + err := Run(context.Background(), project, nil) // Check error - assert.ErrorContains(t, err, "unexpected list functions status 503:") + assert.NoError(t, err) + }) + + t.Run("encodes json format", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputJson + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `[] +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/functions"). + Reply(http.StatusOK). + JSON([]api.FunctionResponse{}) + // Run test + err := Run(context.Background(), project, nil) + // Check error + assert.NoError(t, err) + }) + + t.Run("throws error on env format", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/functions"). + Reply(http.StatusOK). + JSON([]api.FunctionResponse{}) + // Run test + err := Run(context.Background(), project, nil) + // Check error + assert.ErrorIs(t, err, utils.ErrEnvNotSupported) }) t.Run("throws error on network error", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - // Flush pending mocks after test execution - defer gock.OffAll() + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/functions"). - ReplyError(errors.New("network error")) + ReplyError(errNetwork) // Run test - err := Run(context.Background(), project, fsys) + err := Run(context.Background(), project, nil) // Check error - assert.ErrorContains(t, err, "network error") - assert.Empty(t, apitest.ListUnmatchedRequests()) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/functions"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), project, nil) + // Check error + assert.ErrorContains(t, err, "unexpected list functions status 503:") }) } diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go index b0db41ffd4..ba3346413e 100644 --- a/internal/functions/serve/serve.go +++ b/internal/functions/serve/serve.go @@ -126,12 +126,14 @@ func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool, if err != nil { return err } + jwks, _ := utils.Config.Auth.ResolveJWKS(ctx) env = append(env, fmt.Sprintf("SUPABASE_URL=http://%s:8000", utils.KongAliases[0]), "SUPABASE_ANON_KEY="+utils.Config.Auth.AnonKey.Value, "SUPABASE_SERVICE_ROLE_KEY="+utils.Config.Auth.ServiceRoleKey.Value, "SUPABASE_DB_URL="+dbUrl, "SUPABASE_INTERNAL_JWT_SECRET="+utils.Config.Auth.JwtSecret.Value, + "SUPABASE_INTERNAL_JWKS="+jwks, fmt.Sprintf("SUPABASE_INTERNAL_HOST_PORT=%d", utils.Config.Api.Port), ) if viper.GetBool("DEBUG") { @@ -241,6 +243,13 @@ func parseEnvFile(envFilePath string, fsys afero.Fs) ([]string, error) { return env, err } +type dockerFunction struct { + VerifyJWT bool `json:"verifyJWT"` + EntrypointPath string `json:"entrypointPath,omitempty"` + ImportMapPath string `json:"importMapPath,omitempty"` + StaticFiles []string `json:"staticFiles,omitempty"` +} + func PopulatePerFunctionConfigs(cwd, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) ([]string, string, error) { slugs, err := deploy.GetFunctionSlugs(fsys) if err != nil { @@ -251,10 +260,10 @@ func PopulatePerFunctionConfigs(cwd, importMapPath string, noVerifyJWT *bool, fs return nil, "", err } binds := []string{} + enabledFunctions := map[string]dockerFunction{} for slug, fc := range functionsConfig { if !fc.Enabled { fmt.Fprintln(os.Stderr, "Skipped serving Function:", slug) - delete(functionsConfig, slug) continue } modules, err := deploy.GetBindMounts(cwd, utils.FunctionsDir, "", fc.Entrypoint, fc.ImportMap, fsys) @@ -262,14 +271,18 @@ func PopulatePerFunctionConfigs(cwd, importMapPath string, noVerifyJWT *bool, fs return nil, "", err } binds = append(binds, modules...) - fc.ImportMap = utils.ToDockerPath(fc.ImportMap) - fc.Entrypoint = utils.ToDockerPath(fc.Entrypoint) - functionsConfig[slug] = fc + enabled := dockerFunction{ + VerifyJWT: fc.VerifyJWT, + EntrypointPath: utils.ToDockerPath(fc.Entrypoint), + ImportMapPath: utils.ToDockerPath(fc.ImportMap), + StaticFiles: make([]string, len(fc.StaticFiles)), + } for i, val := range fc.StaticFiles { - fc.StaticFiles[i] = utils.ToDockerPath(val) + enabled.StaticFiles[i] = utils.ToDockerPath(val) } + enabledFunctions[slug] = enabled } - functionsConfigBytes, err := json.Marshal(functionsConfig) + functionsConfigBytes, err := json.Marshal(enabledFunctions) if err != nil { return nil, "", errors.Errorf("failed to marshal config json: %w", err) } diff --git a/internal/functions/serve/templates/main.ts b/internal/functions/serve/templates/main.ts index f6676c163d..f9a5febace 100644 --- a/internal/functions/serve/templates/main.ts +++ b/internal/functions/serve/templates/main.ts @@ -1,7 +1,7 @@ import { STATUS_CODE, STATUS_TEXT } from "https://deno.land/std/http/status.ts"; import * as posix from "https://deno.land/std/path/posix/mod.ts"; -import * as jose from "https://deno.land/x/jose@v4.13.1/index.ts"; +import * as jose from "jsr:@panva/jose@6"; const SB_SPECIFIC_ERROR_CODE = { BootError: @@ -29,8 +29,9 @@ const SB_SPECIFIC_ERROR_REASON = { // OS stuff - we don't want to expose these to the functions. const EXCLUDED_ENVS = ["HOME", "HOSTNAME", "PATH", "PWD"]; -const JWT_SECRET = Deno.env.get("SUPABASE_INTERNAL_JWT_SECRET")!; const HOST_PORT = Deno.env.get("SUPABASE_INTERNAL_HOST_PORT")!; +const JWT_SECRET = Deno.env.get("SUPABASE_INTERNAL_JWT_SECRET")!; +const JWKS_ENDPOINT = new URL('/auth/v1/.well-known/jwks.json', Deno.env.get("SUPABASE_URL")!) const DEBUG = Deno.env.get("SUPABASE_INTERNAL_DEBUG") === "true"; const FUNCTIONS_CONFIG_STRING = Deno.env.get( "SUPABASE_INTERNAL_FUNCTIONS_CONFIG", @@ -53,6 +54,7 @@ const GENERIC_FUNCTION_SERVE_MESSAGE = `Serving functions on http://127.0.0.1:${ interface FunctionConfig { entrypointPath: string; importMapPath: string; + staticFiles: string[]; verifyJWT: boolean; } @@ -104,18 +106,62 @@ function getAuthToken(req: Request) { return token; } -async function verifyJWT(jwt: string): Promise { +async function isValidLegacyJWT(jwtSecret: string, jwt: string): Promise { const encoder = new TextEncoder(); - const secretKey = encoder.encode(JWT_SECRET); + const secretKey = encoder.encode(jwtSecret); try { await jose.jwtVerify(jwt, secretKey); } catch (e) { - console.error(e); + console.error('Symmetric Legacy JWT verification error', e); + return false; + } + return true; +} + +// Lazy-loading JWKs +let jwks = (() => { + try { + // using injected JWKS from cli + return jose.createLocalJWKSet(JSON.parse(Deno.env.get('SUPABASE_INTERNAL_JWKS'))); + } catch (error) { + return null + } +})(); + +async function isValidJWT(jwksUrl: string, jwt: string): Promise { + try { + if (!jwks) { + // Loading from remote-url on fly + jwks = jose.createRemoteJWKSet(new URL(jwksUrl)); + } + await jose.jwtVerify(jwt, jwks); + } catch (e) { + console.error('Asymmetric JWT verification error', e); return false; } return true; } +/** + * Applies hybrid JWT verification, using JWK as primary and Legacy Secret as fallback. + * Use only during 'New JWT Keys' migration period, while `JWT_SECRET` is still available. + */ +export async function verifyHybridJWT(jwtSecret: string, jwksUrl: string, jwt: string): Promise { + const { alg: jwtAlgorithm } = jose.decodeProtectedHeader(jwt) + + if (jwtAlgorithm === 'HS256') { + console.log(`Legacy token type detected, attempting ${jwtAlgorithm} verification.`) + + return await isValidLegacyJWT(jwtSecret, jwt) + } + + if (jwtAlgorithm === 'ES256' || jwtAlgorithm === 'RS256') { + return await isValidJWT(jwksUrl, jwt) + } + + return false; +} + // Ref: https://docs.deno.com/examples/checking_file_existence/ async function shouldUsePackageJsonDiscovery({ entrypointPath, importMapPath }: FunctionConfig): Promise { if (importMapPath) { @@ -158,7 +204,7 @@ Deno.serve({ if (req.method !== "OPTIONS" && functionsConfig[functionName].verifyJWT) { try { const token = getAuthToken(req); - const isValidJWT = await verifyJWT(token); + const isValidJWT = await verifyHybridJWT(JWT_SECRET, JWKS_ENDPOINT, token); if (!isValidJWT) { return getResponse({ msg: "Invalid JWT" }, STATUS_CODE.Unauthorized); diff --git a/internal/gen/signingkeys/signingkeys.go b/internal/gen/signingkeys/signingkeys.go index 5651ac2651..b1fdb9e73c 100644 --- a/internal/gen/signingkeys/signingkeys.go +++ b/internal/gen/signingkeys/signingkeys.go @@ -9,7 +9,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io" "math/big" "os" "path/filepath" @@ -102,7 +101,6 @@ func Run(ctx context.Context, algorithm string, appendMode bool, fsys afero.Fs) if err != nil { return err } - outputPath := utils.Config.Auth.SigningKeysPath // Generate key pair privateJWK, err := GeneratePrivateKey(config.Algorithm(algorithm)) @@ -111,7 +109,7 @@ func Run(ctx context.Context, algorithm string, appendMode bool, fsys afero.Fs) } // Only serialise a single key to stdout - if len(outputPath) == 0 { + if len(utils.Config.Auth.SigningKeysPath) == 0 { enc := json.NewEncoder(os.Stdout) if err := enc.Encode(privateJWK); err != nil { return errors.Errorf("failed to encode signing key: %w", err) @@ -127,51 +125,25 @@ signing_keys_path = "./signing_key.json" return nil } - var jwkArray []config.JWK - if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(outputPath)); err != nil { - return err - } - f, err := fsys.OpenFile(outputPath, os.O_RDWR|os.O_CREATE, 0600) - if err != nil { - return errors.Errorf("failed to open signing key: %w", err) - } - defer f.Close() if appendMode { - // Load existing key and reset file - dec := json.NewDecoder(f) - // Since a new file is empty, we must ignore EOF error - if err := dec.Decode(&jwkArray); err != nil && !errors.Is(err, io.EOF) { - return errors.Errorf("failed to decode signing key: %w", err) - } - if _, err = f.Seek(0, io.SeekStart); err != nil { - return errors.Errorf("failed to seek signing key: %w", err) - } - } else if fi, err := f.Stat(); fi.Size() > 0 { - if err != nil { - fmt.Fprintln(utils.GetDebugLogger(), err) - } - label := fmt.Sprintf("Do you want to overwrite the existing %s file?", utils.Bold(outputPath)) + utils.Config.Auth.SigningKeys = append(utils.Config.Auth.SigningKeys, *privateJWK) + } else { + label := fmt.Sprintf("Do you want to overwrite the existing %s file?", utils.Bold(utils.Config.Auth.SigningKeysPath)) if shouldOverwrite, err := utils.NewConsole().PromptYesNo(ctx, label, true); err != nil { return err } else if !shouldOverwrite { return errors.New(context.Canceled) } - if err := f.Truncate(0); err != nil { - return errors.Errorf("failed to truncate signing key: %w", err) - } + utils.Config.Auth.SigningKeys = []config.JWK{*privateJWK} } - jwkArray = append(jwkArray, *privateJWK) - // Write to file - enc := json.NewEncoder(f) - enc.SetIndent("", " ") - if err := enc.Encode(jwkArray); err != nil { - return errors.Errorf("failed to encode signing key: %w", err) + if err := saveSigningKeys(fsys); err != nil { + return err } - fmt.Fprintf(os.Stderr, "JWT signing key appended to: %s (now contains %d keys)\n", utils.Bold(outputPath), len(jwkArray)) - if len(jwkArray) == 1 { - if ignored, err := utils.IsGitIgnored(outputPath); err != nil { + fmt.Fprintf(os.Stderr, "JWT signing key appended to: %s (now contains %d keys)\n", utils.Bold(utils.Config.Auth.SigningKeysPath), len(utils.Config.Auth.SigningKeys)) + if len(utils.Config.Auth.SigningKeys) == 1 { + if ignored, err := utils.IsGitIgnored(utils.Config.Auth.SigningKeysPath); err != nil { fmt.Fprintln(utils.GetDebugLogger(), err) } else if !ignored { // Since the output path is user defined, we can't update the managed .gitignore file. @@ -181,6 +153,20 @@ signing_keys_path = "./signing_key.json" return nil } +func saveSigningKeys(fsys afero.Fs) error { + f, err := fsys.OpenFile(utils.Config.Auth.SigningKeysPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return errors.Errorf("failed to open signing key: %w", err) + } + defer f.Close() + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + if err := enc.Encode(utils.Config.Auth.SigningKeys); err != nil { + return errors.Errorf("failed to encode signing key: %w", err) + } + return nil +} + // GetSupportedAlgorithms returns a list of supported algorithms func GetSupportedAlgorithms() []string { 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 6c3a7c63d5..b03d8277f2 100644 --- a/internal/gen/signingkeys/signingkeys_test.go +++ b/internal/gen/signingkeys/signingkeys_test.go @@ -1,8 +1,16 @@ package signingkeys import ( + "context" + "os" + "path/filepath" "testing" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/config" ) @@ -110,3 +118,69 @@ func TestGetSupportedAlgorithms(t *testing.T) { } } } + +func TestSigningKey(t *testing.T) { + t.Run("generates signing key", func(t *testing.T) { + fsys := afero.NewMemMapFs() + err := Run(context.Background(), "ES256", false, fsys) + assert.NoError(t, err) + }) + + t.Run("throws error on permission denied", func(t *testing.T) { + fsys := &fstest.OpenErrorFs{DenyPath: utils.ConfigPath} + err := Run(context.Background(), "ES256", false, fsys) + assert.ErrorIs(t, err, os.ErrPermission) + }) + + t.Run("throws error on encode failure", func(t *testing.T) { + oldStdout := os.Stdout + t.Cleanup(func() { os.Stdout = oldStdout }) + os.Stdout = nil + // Run test + fsys := afero.NewMemMapFs() + err := Run(context.Background(), "ES256", false, fsys) + assert.ErrorIs(t, err, os.ErrInvalid) + }) +} + +func TestAppendKey(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() + keyPath := filepath.Join(utils.SupabaseDirPath, "signing_keys.json") + require.NoError(t, utils.WriteFile(keyPath, []byte(`[]`), fsys)) + require.NoError(t, utils.WriteFile(utils.ConfigPath, []byte(` +[auth] +signing_keys_path = "./signing_keys.json" +`), fsys)) + + t.Run("overwrites signing key", func(t *testing.T) { + t.Cleanup(fstest.MockStdin(t, "y")) + err := Run(context.Background(), "ES256", false, fsys) + assert.NoError(t, err) + }) + + t.Run("throws error on context cancelled", func(t *testing.T) { + t.Cleanup(fstest.MockStdin(t, "y")) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + // Run test + err := Run(ctx, "ES256", false, afero.NewReadOnlyFs(fsys)) + assert.ErrorIs(t, err, context.Canceled) + }) + + t.Run("throws error on no answer", func(t *testing.T) { + t.Cleanup(fstest.MockStdin(t, "n")) + err := Run(context.Background(), "ES256", false, afero.NewReadOnlyFs(fsys)) + assert.ErrorIs(t, err, context.Canceled) + }) + + t.Run("appends signing key", func(t *testing.T) { + err := Run(context.Background(), "ES256", true, fsys) + assert.NoError(t, err) + }) + + t.Run("throws error on write failure", func(t *testing.T) { + err := Run(context.Background(), "ES256", true, afero.NewReadOnlyFs(fsys)) + assert.ErrorIs(t, err, os.ErrPermission) + }) +} diff --git a/internal/gen/types/types.go b/internal/gen/types/types.go index fef6a97726..e5e7d73633 100644 --- a/internal/gen/types/types.go +++ b/internal/gen/types/types.go @@ -129,7 +129,22 @@ func GetRootCA(ctx context.Context, dbURL string, options ...func(*pgx.ConnConfi } func isRequireSSL(ctx context.Context, dbUrl string, options ...func(*pgx.ConnConfig)) (bool, error) { - conn, err := utils.ConnectByUrl(ctx, dbUrl+"&sslmode=require", options...) + + // pgx v4's sslmode=require verifies the server certificate against system CAs, + // unlike libpq where require skips verification. When SUPABASE_CA_SKIP_VERIFY=true, + // skip verification for this probe only (detects whether the server speaks TLS). + // Cert validation happens downstream in the migra/pgdelta Deno scripts using GetRootCA. + opts := options + if os.Getenv("SUPABASE_CA_SKIP_VERIFY") == "true" { + opts = append(opts, func(cc *pgx.ConnConfig) { + if cc.TLSConfig != nil { + // #nosec G402 -- Intentionally skipped for this TLS capability probe only. + // Downstream migra/pgdelta flows still validate certificates using GetRootCA. + cc.TLSConfig.InsecureSkipVerify = true + } + }) + } + conn, err := utils.ConnectByUrl(ctx, dbUrl+"&sslmode=require", opts...) if err != nil { if strings.HasSuffix(err.Error(), "(server refused TLS connection)") { return false, nil diff --git a/internal/hostnames/activate/activate.go b/internal/hostnames/activate/activate.go index c2306a29ee..11a5e41248 100644 --- a/internal/hostnames/activate/activate.go +++ b/internal/hostnames/activate/activate.go @@ -2,7 +2,7 @@ package activate import ( "context" - "fmt" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -10,33 +10,16 @@ import ( "github.com/supabase/cli/internal/utils" ) -func Run(ctx context.Context, projectRef string, includeRawOutput bool, fsys afero.Fs) error { - // 1. Sanity checks. - { - resp, err := hostnames.GetCustomHostnameConfig(ctx, projectRef) - if err != nil { - return err - } - err = hostnames.VerifyCNAME(ctx, projectRef, resp.JSON200.CustomHostname) - if err != nil { - return err - } +func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { + resp, err := utils.GetSupabase().V1ActivateCustomHostnameWithResponse(ctx, projectRef) + if err != nil { + return errors.Errorf("failed to activate custom hostname: %w", err) + } else if resp.JSON201 == nil { + return errors.Errorf("unexpected activate hostname status %d: %s", resp.StatusCode(), string(resp.Body)) } - - // 2. activate custom hostname config - { - resp, err := utils.GetSupabase().V1ActivateCustomHostnameWithResponse(ctx, projectRef) - if err != nil { - 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)) - } - status, err := hostnames.TranslateStatus(resp.JSON201, includeRawOutput) - if err != nil { - return err - } - fmt.Println(status) - return nil + hostnames.PrintStatus(resp.JSON201, os.Stderr) + if utils.OutputFormat.Value != utils.OutputPretty { + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON201) } + return nil } diff --git a/internal/hostnames/activate/activate_test.go b/internal/hostnames/activate/activate_test.go new file mode 100644 index 0000000000..104885421d --- /dev/null +++ b/internal/hostnames/activate/activate_test.go @@ -0,0 +1,90 @@ +package activate + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestActivateHostname(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("activate custom hostname", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/custom-hostname/activate"). + Reply(http.StatusCreated). + JSON(api.UpdateCustomHostnameResponse{}) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `CustomHostname = "" +Status = "4_origin_setup_completed" + +[Data] + Success = false + [Data.Result] + CustomOriginServer = "" + Hostname = "" + Id = "" + Status = "" + [Data.Result.OwnershipVerification] + Name = "" + Type = "" + Value = "" + [Data.Result.Ssl] + Status = "" +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/custom-hostname/activate"). + Reply(http.StatusCreated). + JSON(api.UpdateCustomHostnameResponse{ + Status: api.N4OriginSetupCompleted, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/custom-hostname/activate"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/custom-hostname/activate"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected activate hostname status 503:") + }) +} diff --git a/internal/hostnames/common.go b/internal/hostnames/common.go index c01e6a0da3..f991e7a652 100644 --- a/internal/hostnames/common.go +++ b/internal/hostnames/common.go @@ -2,8 +2,8 @@ package hostnames import ( "context" - "encoding/json" "fmt" + "io" "strings" "github.com/go-errors/errors" @@ -11,125 +11,49 @@ import ( "github.com/supabase/cli/pkg/api" ) -func GetCustomHostnameConfig(ctx context.Context, projectRef string) (*api.V1GetHostnameConfigResponse, error) { - resp, err := utils.GetSupabase().V1GetHostnameConfigWithResponse(ctx, projectRef) - if err != nil { - return nil, errors.Errorf("failed to get custom hostname: %w", err) - } - if resp.JSON200 == nil { - return nil, errors.New("failed to get custom hostname config; received: " + string(resp.Body)) - } - return resp, nil -} - func VerifyCNAME(ctx context.Context, projectRef string, customHostname string) error { expectedEndpoint := fmt.Sprintf("%s.", utils.GetSupabaseHost(projectRef)) - cname, err := utils.ResolveCNAME(ctx, customHostname) - if err != nil { + if cname, err := utils.ResolveCNAME(ctx, customHostname); err != nil { return errors.Errorf("expected custom hostname '%s' to have a CNAME record pointing to your project at '%s', but it failed to resolve: %w", customHostname, expectedEndpoint, err) - } - if cname != expectedEndpoint { + } else if cname != expectedEndpoint { return errors.Errorf("expected custom hostname '%s' to have a CNAME record pointing to your project at '%s', but it is currently set to '%s'", customHostname, expectedEndpoint, cname) } return nil } -type RawResponse struct { - Result struct { - CustomOriginServer string `json:"custom_origin_server"` - OwnershipVerification struct { - Name string - Type string - Value string - } `json:"ownership_verification"` - Ssl struct { - ValidationRecords []struct { - Status string `json:"status"` - TxtName string `json:"txt_name"` - TxtValue string `json:"txt_value"` - } `json:"validation_records"` - ValidationErrors []struct { - Message string `json:"message"` - } `json:"validation_errors"` - Status string `json:"status"` - } - } `json:"result"` -} - -func serializeRawOutput(response *api.UpdateCustomHostnameResponse) (string, error) { - output, err := json.MarshalIndent(response, "", " ") - if err != nil { - return "", errors.Errorf("failed to serialize json: %w", err) - } - return string(output), nil -} - -func appendRawOutputIfNeeded(status string, response *api.UpdateCustomHostnameResponse, includeRawOutput bool) string { - if !includeRawOutput { - return status - } - rawOutput, err := serializeRawOutput(response) - if err != nil { - return fmt.Sprintf("%s\nFailed to serialize raw output: %+v\n", status, err) - } - return fmt.Sprintf("%s\nRaw output follows:\n%s\n", status, rawOutput) -} - -func TranslateStatus(response *api.UpdateCustomHostnameResponse, includeRawOutput bool) (string, error) { - if response.Status == api.N5ServicesReconfigured { - return appendRawOutputIfNeeded(fmt.Sprintf("Custom hostname setup completed. Project is now accessible at %s.", response.CustomHostname), response, includeRawOutput), nil - } - if response.Status == api.N4OriginSetupCompleted { - var res RawResponse - rawBody, err := json.Marshal(response.Data) - if err != nil { - return "", errors.Errorf("failed to serialize body: %w", err) - } - err = json.Unmarshal(rawBody, &res) - if err != nil { - return "", errors.Errorf("failed to deserialize body: %w", err) - } - return appendRawOutputIfNeeded(fmt.Sprintf(`Custom hostname configuration complete, and ready for activation. +func PrintStatus(response *api.UpdateCustomHostnameResponse, w io.Writer) { + switch response.Status { + case api.N5ServicesReconfigured: + fmt.Fprintf(w, "Custom hostname setup completed. Project is now accessible at %s.", response.CustomHostname) + case api.N4OriginSetupCompleted: + fmt.Fprintf(w, `Custom hostname configuration complete, and ready for activation. Please ensure that your custom domain is set up as a CNAME record to your Supabase subdomain: - %s CNAME -> %s`, response.CustomHostname, res.Result.CustomOriginServer), response, includeRawOutput), nil - } - if response.Status == api.N2Initiated { - var res RawResponse - rawBody, err := json.Marshal(response.Data) - if err != nil { - return "", errors.Errorf("failed to serialize body: %w", err) - } - err = json.Unmarshal(rawBody, &res) - if err != nil { - return "", errors.Errorf("failed to deserialize body: %w", err) - } - ssl := res.Result.Ssl.ValidationRecords - if res.Result.Ssl.Status == "initializing" { - return appendRawOutputIfNeeded("Custom hostname setup is being initialized; please request re-verification in a few seconds.\n", response, includeRawOutput), nil - } - if len(res.Result.Ssl.ValidationErrors) > 0 { +%s CNAME -> %s`, response.CustomHostname, response.Data.Result.CustomOriginServer) + case api.N3ChallengeVerified, api.N2Initiated: + if ssl := response.Data.Result.Ssl; ssl.Status == "initializing" { + fmt.Fprintln(w, "Custom hostname setup is being initialized; please request re-verification in a few seconds.") + } else if errVal := response.Data.Result.Ssl.ValidationErrors; errVal != nil && len(*errVal) > 0 { var errorMessages []string - for _, valError := range res.Result.Ssl.ValidationErrors { + for _, valError := range *errVal { if strings.Contains(valError.Message, "caa_error") { - return appendRawOutputIfNeeded("CAA mismatch; please remove any existing CAA records on your domain, or add one for \"digicert.com\"\n", response, includeRawOutput), nil + fmt.Fprintln(w, `CAA mismatch; please remove any existing CAA records on your domain, or add one for "digicert.com"`) + return } errorMessages = append(errorMessages, valError.Message) } valErrors := strings.Join(errorMessages, "\n\t- ") - return appendRawOutputIfNeeded(fmt.Sprintf("SSL validation errors: \n\t- %s\n", valErrors), response, includeRawOutput), nil - } - if len(ssl) != 1 { - return "", errors.Errorf("expected a single SSL verification record, received: %+v", ssl) - } - records := "" - if ssl[0].TxtName != "" { - records = fmt.Sprintf("%s\n\t%s TXT -> %s", records, ssl[0].TxtName, ssl[0].TxtValue) + fmt.Fprintf(w, "SSL validation errors: \n\t- %s\n", valErrors) + } else if len(ssl.ValidationRecords) != 1 { + fmt.Fprintf(w, "expected a single SSL verification record, received: %+v", ssl) + } else { + fmt.Fprintln(w, `Custom hostname verification in-progress; please configure the appropriate DNS entries and request re-verification. +Required outstanding validation records:`) + if rec := ssl.ValidationRecords[0]; rec.TxtName != "" { + fmt.Fprintf(w, "\t%s TXT -> %s", rec.TxtName, rec.TxtValue) + } } - status := fmt.Sprintf("Custom hostname verification in-progress; please configure the appropriate DNS entries and request re-verification.\n"+ - "Required outstanding validation records: %s\n", - records) - return appendRawOutputIfNeeded(status, response, includeRawOutput), nil + case api.N1NotStarted: + fmt.Fprintln(w, "Custom hostname configuration not started.") } - return appendRawOutputIfNeeded("Custom hostname configuration not started.", response, includeRawOutput), nil } diff --git a/internal/hostnames/common_test.go b/internal/hostnames/common_test.go index 2c3755c7c0..4502ef2fe7 100644 --- a/internal/hostnames/common_test.go +++ b/internal/hostnames/common_test.go @@ -1,6 +1,7 @@ package hostnames import ( + "bytes" "context" "net/http" "testing" @@ -8,6 +9,7 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/pkg/api" ) func TestVerifyCNAME(t *testing.T) { @@ -44,3 +46,55 @@ func TestVerifyCNAMEFailures(t *testing.T) { err := VerifyCNAME(context.Background(), "foobarbaz", "hello.custom-domain.com") assert.ErrorContains(t, err, "failed to locate appropriate CNAME record for hello.custom-domain.com") } + +func TestPrintStatus(t *testing.T) { + t.Run("initialising hostname message", func(t *testing.T) { + data := api.UpdateCustomHostnameResponse{Status: api.N2Initiated} + data.Data.Result.Ssl.Status = "initializing" + var buf bytes.Buffer + PrintStatus(&data, &buf) + assert.Equal(t, "Custom hostname setup is being initialized; please request re-verification in a few seconds.\n", buf.String()) + }) + + t.Run("validation records message", func(t *testing.T) { + data := api.UpdateCustomHostnameResponse{Status: api.N2Initiated} + data.Data.Result.Ssl.ValidationRecords = []struct { + TxtName string `json:"txt_name"` + TxtValue string `json:"txt_value"` + }{{ + TxtName: "_pki-validation", + TxtValue: "f3k9d8s7h2l1m4n6p0q5r", + }} + var buf bytes.Buffer + PrintStatus(&data, &buf) + assert.Equal(t, `Custom hostname verification in-progress; please configure the appropriate DNS entries and request re-verification. +Required outstanding validation records: + _pki-validation TXT -> f3k9d8s7h2l1m4n6p0q5r`, buf.String()) + }) + + t.Run("validation error message", func(t *testing.T) { + data := api.UpdateCustomHostnameResponse{Status: api.N2Initiated} + data.Data.Result.Ssl.ValidationErrors = &[]struct { + Message string `json:"message"` + }{{ + Message: "self signed cert", + }} + var buf bytes.Buffer + PrintStatus(&data, &buf) + assert.Equal(t, `SSL validation errors: + - self signed cert +`, buf.String()) + }) + + t.Run("validation caa error message", func(t *testing.T) { + data := api.UpdateCustomHostnameResponse{Status: api.N2Initiated} + data.Data.Result.Ssl.ValidationErrors = &[]struct { + Message string `json:"message"` + }{{ + Message: "caa_error", + }} + var buf bytes.Buffer + PrintStatus(&data, &buf) + assert.Equal(t, "CAA mismatch; please remove any existing CAA records on your domain, or add one for \"digicert.com\"\n", buf.String()) + }) +} diff --git a/internal/hostnames/create/create.go b/internal/hostnames/create/create.go index 563b524bc7..ef6f01e603 100644 --- a/internal/hostnames/create/create.go +++ b/internal/hostnames/create/create.go @@ -2,8 +2,7 @@ package create import ( "context" - "fmt" - "strings" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -12,36 +11,24 @@ import ( "github.com/supabase/cli/pkg/api" ) -func Run(ctx context.Context, projectRef string, customHostname string, includeRawOutput bool, fsys afero.Fs) error { - // 1. Sanity checks. - hostname := strings.TrimSpace(customHostname) - { - if len(hostname) == 0 { - return errors.New("non-empty custom hostname expected") - } - // we verify that a CNAME is set as it simplifies the checks used for verifying ownership - err := hostnames.VerifyCNAME(ctx, projectRef, hostname) - if err != nil { - return err - } +func Run(ctx context.Context, projectRef string, customHostname string, fsys afero.Fs) error { + // 1. verify that a CNAME is set as it simplifies the checks used for verifying ownership + err := hostnames.VerifyCNAME(ctx, projectRef, customHostname) + if err != nil { + return err } - // 2. create custom hostname - { - resp, err := utils.GetSupabase().V1UpdateHostnameConfigWithResponse(ctx, projectRef, api.V1UpdateHostnameConfigJSONRequestBody{ - CustomHostname: hostname, - }) - if err != nil { - return errors.Errorf("failed to create custom hostname: %w", err) - } - if resp.JSON201 == nil { - return errors.New("failed to create custom hostname config: " + string(resp.Body)) - } - status, err := hostnames.TranslateStatus(resp.JSON201, includeRawOutput) - if err != nil { - return err - } - fmt.Println(status) - return nil + resp, err := utils.GetSupabase().V1UpdateHostnameConfigWithResponse(ctx, projectRef, api.V1UpdateHostnameConfigJSONRequestBody{ + CustomHostname: customHostname, + }) + if err != nil { + return errors.Errorf("failed to create custom hostname: %w", err) + } else if resp.JSON201 == nil { + return errors.Errorf("unexpected create hostname status %d: %s", resp.StatusCode(), string(resp.Body)) + } + hostnames.PrintStatus(resp.JSON201, os.Stderr) + if utils.OutputFormat.Value != utils.OutputPretty { + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON201) } + return nil } diff --git a/internal/hostnames/create/create_test.go b/internal/hostnames/create/create_test.go new file mode 100644 index 0000000000..64d3ffd0a0 --- /dev/null +++ b/internal/hostnames/create/create_test.go @@ -0,0 +1,127 @@ +package create + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/cloudflare" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestCreateHostname(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("create custom hostname", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New("https://1.1.1.1"). + Get("/dns-query"). + Reply(http.StatusOK). + JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{ + {Type: cloudflare.TypeCNAME, Data: flags.ProjectRef + ".."}, + }}) + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/custom-hostname"). + Reply(http.StatusCreated). + JSON(api.UpdateCustomHostnameResponse{}) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `CustomHostname = "" +Status = "1_not_started" + +[Data] + Success = false + [Data.Result] + CustomOriginServer = "" + Hostname = "" + Id = "" + Status = "" + [Data.Result.OwnershipVerification] + Name = "" + Type = "" + Value = "" + [Data.Result.Ssl] + Status = "" +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New("https://1.1.1.1"). + Get("/dns-query"). + Reply(http.StatusOK). + JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{ + {Type: cloudflare.TypeCNAME, Data: flags.ProjectRef + ".."}, + }}) + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/custom-hostname"). + Reply(http.StatusCreated). + JSON(api.UpdateCustomHostnameResponse{ + Status: api.N1NotStarted, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.NoError(t, err) + }) + + t.Run("throws error on invalid cname", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New("https://1.1.1.1"). + Get("/dns-query"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New("https://1.1.1.1"). + Get("/dns-query"). + Reply(http.StatusOK). + JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{ + {Type: cloudflare.TypeCNAME, Data: flags.ProjectRef + ".."}, + }}) + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/custom-hostname"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New("https://1.1.1.1"). + Get("/dns-query"). + Reply(http.StatusOK). + JSON(&cloudflare.DNSResponse{Answer: []cloudflare.DNSAnswer{ + {Type: cloudflare.TypeCNAME, Data: flags.ProjectRef + ".."}, + }}) + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/custom-hostname"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.ErrorContains(t, err, "unexpected create hostname status 503:") + }) +} diff --git a/internal/hostnames/delete/delete.go b/internal/hostnames/delete/delete.go index 3b9f3653c1..fdc3a18032 100644 --- a/internal/hostnames/delete/delete.go +++ b/internal/hostnames/delete/delete.go @@ -3,6 +3,8 @@ package delete import ( "context" "fmt" + "net/http" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -10,17 +12,12 @@ import ( ) func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { - // 1. Sanity checks. - // 2. delete config - { - resp, err := utils.GetSupabase().V1DeleteHostnameConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to delete custom hostname: %w", err) - } - if resp.StatusCode() != 200 { - return errors.New("failed to delete custom hostname config; received: " + resp.Status()) - } - fmt.Println("Deleted custom hostname config successfully.") - return nil + resp, err := utils.GetSupabase().V1DeleteHostnameConfigWithResponse(ctx, projectRef) + if err != nil { + return errors.Errorf("failed to delete custom hostname: %w", err) + } else if resp.StatusCode() != http.StatusOK { + return errors.Errorf("unexpected delete hostname status %d: %s", resp.StatusCode(), string(resp.Body)) } + fmt.Fprintln(os.Stderr, "Deleted custom hostname config successfully.") + return nil } diff --git a/internal/hostnames/delete/delete_test.go b/internal/hostnames/delete/delete_test.go new file mode 100644 index 0000000000..667d12a39a --- /dev/null +++ b/internal/hostnames/delete/delete_test.go @@ -0,0 +1,56 @@ +package delete + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestDeleteHostname(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("deletes custom hostname", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("v1/projects/" + flags.ProjectRef + "/custom-hostname"). + Reply(http.StatusOK). + JSON(api.UpdateCustomHostnameResponse{}) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("v1/projects/" + flags.ProjectRef + "/custom-hostname"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("v1/projects/" + flags.ProjectRef + "/custom-hostname"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected delete hostname status 503:") + }) +} diff --git a/internal/hostnames/get/get.go b/internal/hostnames/get/get.go index 9e17c19f2d..ded7c8622f 100644 --- a/internal/hostnames/get/get.go +++ b/internal/hostnames/get/get.go @@ -2,25 +2,24 @@ package get import ( "context" - "fmt" + "os" + "github.com/go-errors/errors" "github.com/spf13/afero" "github.com/supabase/cli/internal/hostnames" + "github.com/supabase/cli/internal/utils" ) -func Run(ctx context.Context, projectRef string, includeRawOutput bool, fsys afero.Fs) error { - // 1. Sanity checks. - // 2. activate custom hostname config - { - resp, err := hostnames.GetCustomHostnameConfig(ctx, projectRef) - if err != nil { - return err - } - status, err := hostnames.TranslateStatus(resp.JSON200, includeRawOutput) - if err != nil { - return err - } - fmt.Println(status) - return nil +func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { + resp, err := utils.GetSupabase().V1GetHostnameConfigWithResponse(ctx, projectRef) + if err != nil { + return errors.Errorf("failed to get custom hostname: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected get hostname status %d: %s", resp.StatusCode(), string(resp.Body)) } + hostnames.PrintStatus(resp.JSON200, os.Stderr) + if utils.OutputFormat.Value != utils.OutputPretty { + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON200) + } + return nil } diff --git a/internal/hostnames/get/get_test.go b/internal/hostnames/get/get_test.go new file mode 100644 index 0000000000..9fe5a60e35 --- /dev/null +++ b/internal/hostnames/get/get_test.go @@ -0,0 +1,90 @@ +package get + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestGetHostname(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("fetches custom hostname", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/custom-hostname"). + Reply(http.StatusOK). + JSON(api.UpdateCustomHostnameResponse{}) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `CustomHostname = "" +Status = "5_services_reconfigured" + +[Data] + Success = false + [Data.Result] + CustomOriginServer = "" + Hostname = "" + Id = "" + Status = "" + [Data.Result.OwnershipVerification] + Name = "" + Type = "" + Value = "" + [Data.Result.Ssl] + Status = "" +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/custom-hostname"). + Reply(http.StatusOK). + JSON(api.UpdateCustomHostnameResponse{ + Status: api.N5ServicesReconfigured, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/custom-hostname"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/custom-hostname"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected get hostname status 503:") + }) +} diff --git a/internal/hostnames/reverify/reverify.go b/internal/hostnames/reverify/reverify.go index 7f4329903e..f38f38fd49 100644 --- a/internal/hostnames/reverify/reverify.go +++ b/internal/hostnames/reverify/reverify.go @@ -2,7 +2,7 @@ package reverify import ( "context" - "fmt" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -10,22 +10,16 @@ import ( "github.com/supabase/cli/internal/utils" ) -func Run(ctx context.Context, projectRef string, includeRawOutput bool, fsys afero.Fs) error { - // 1. Sanity checks. - // 2. attempt to re-verify custom hostname config - { - resp, err := utils.GetSupabase().V1VerifyDnsConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to re-verify custom hostname: %w", err) - } - if resp.JSON201 == nil { - return errors.New("failed to re-verify custom hostname config: " + string(resp.Body)) - } - status, err := hostnames.TranslateStatus(resp.JSON201, includeRawOutput) - if err != nil { - return err - } - fmt.Println(status) - return nil +func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { + resp, err := utils.GetSupabase().V1VerifyDnsConfigWithResponse(ctx, projectRef) + if err != nil { + return errors.Errorf("failed to re-verify custom hostname: %w", err) + } else if resp.JSON201 == nil { + return errors.Errorf("unexpected re-verify hostname status %d: %s", resp.StatusCode(), string(resp.Body)) } + hostnames.PrintStatus(resp.JSON201, os.Stderr) + if utils.OutputFormat.Value != utils.OutputPretty { + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON201) + } + return nil } diff --git a/internal/hostnames/reverify/reverify_test.go b/internal/hostnames/reverify/reverify_test.go new file mode 100644 index 0000000000..f0b6f4fd16 --- /dev/null +++ b/internal/hostnames/reverify/reverify_test.go @@ -0,0 +1,90 @@ +package reverify + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestReverifyHostname(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("reverify custom hostname", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/custom-hostname/reverify"). + Reply(http.StatusCreated). + JSON(api.UpdateCustomHostnameResponse{}) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `CustomHostname = "" +Status = "2_initiated" + +[Data] + Success = false + [Data.Result] + CustomOriginServer = "" + Hostname = "" + Id = "" + Status = "" + [Data.Result.OwnershipVerification] + Name = "" + Type = "" + Value = "" + [Data.Result.Ssl] + Status = "" +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/custom-hostname/reverify"). + Reply(http.StatusCreated). + JSON(api.UpdateCustomHostnameResponse{ + Status: api.N2Initiated, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/custom-hostname/reverify"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/custom-hostname/reverify"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected re-verify hostname status 503:") + }) +} diff --git a/internal/postgresConfig/delete/delete.go b/internal/postgresConfig/delete/delete.go index 20b7b978f5..86ecee6304 100644 --- a/internal/postgresConfig/delete/delete.go +++ b/internal/postgresConfig/delete/delete.go @@ -35,14 +35,15 @@ func Run(ctx context.Context, projectRef string, configKeys []string, noRestart resp, err := utils.GetSupabase().V1UpdatePostgresConfigWithBodyWithResponse(ctx, projectRef, "application/json", bytes.NewReader(bts)) if err != nil { - return errors.Errorf("failed to update config overrides: %w", err) - } - if resp.JSON200 == nil { - if resp.StatusCode() == 400 { - return errors.Errorf("failed to update config overrides: %s (%s). This usually indicates that an unsupported or invalid config override was attempted. Please refer to https://supabase.com/docs/guides/platform/custom-postgres-config", resp.Status(), string(resp.Body)) - } - return errors.Errorf("failed to update config overrides: %s (%s)", resp.Status(), string(resp.Body)) + return errors.Errorf("failed to delete config overrides: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected delete config overrides status %d: %s", resp.StatusCode(), string(resp.Body)) } - return get.Run(ctx, projectRef, fsys) + var config map[string]any + err = json.Unmarshal(resp.Body, &config) + if err != nil { + return errors.Errorf("failed to unmarshal delete response: %w", err) + } + return get.PrintOutPostgresConfigOverrides(config) } diff --git a/internal/postgresConfig/delete/delete_test.go b/internal/postgresConfig/delete/delete_test.go new file mode 100644 index 0000000000..e1bfa880bb --- /dev/null +++ b/internal/postgresConfig/delete/delete_test.go @@ -0,0 +1,94 @@ +package delete + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" + "github.com/supabase/cli/pkg/cast" +) + +func TestDeleteConfig(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("deletes postgres config", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, ` + + Parameter | Value + -----------|------- + +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + gock.New(utils.DefaultApiHost). + Put("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{}) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{"max_connections"}, true, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on missing project", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{}, false, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{}, false, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{}, false, nil) + assert.ErrorContains(t, err, "unexpected delete config overrides status 503:") + }) +} diff --git a/internal/postgresConfig/get/get.go b/internal/postgresConfig/get/get.go index 01a749ce45..e2142cf183 100644 --- a/internal/postgresConfig/get/get.go +++ b/internal/postgresConfig/get/get.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "io" "os" "strings" @@ -14,23 +13,18 @@ import ( ) func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { - // 1. get current config config, err := GetCurrentPostgresConfig(ctx, projectRef) if err != nil { return err } - err = PrintOutPostgresConfigOverrides(config) - if err != nil { - return err - } - return nil + return PrintOutPostgresConfigOverrides(config) } func PrintOutPostgresConfigOverrides(config map[string]any) error { if utils.OutputFormat.Value != utils.OutputPretty { return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, config) } - fmt.Println("- Custom Postgres Config -") + fmt.Fprintln(os.Stderr, "- Custom Postgres Config -") markdownTable := []string{ "|Parameter|Value|\n|-|-|\n", } @@ -43,26 +37,21 @@ func PrintOutPostgresConfigOverrides(config map[string]any) error { if err := utils.RenderTable(strings.Join(markdownTable, "")); err != nil { return err } - fmt.Println("- End of Custom Postgres Config -") + fmt.Fprintln(os.Stderr, "- End of Custom Postgres Config -") return nil } func GetCurrentPostgresConfig(ctx context.Context, projectRef string) (map[string]any, error) { - resp, err := utils.GetSupabase().V1GetPostgresConfig(ctx, projectRef) + resp, err := utils.GetSupabase().V1GetPostgresConfigWithResponse(ctx, projectRef) if err != nil { return nil, errors.Errorf("failed to retrieve Postgres config overrides: %w", err) - } - if resp.StatusCode != 200 { - return nil, errors.Errorf("error in retrieving Postgres config overrides: %s", resp.Status) - } - contents, err := io.ReadAll(resp.Body) - if err != nil { - return nil, errors.Errorf("failed to read response body: %w", err) + } else if resp.JSON200 == nil { + return nil, errors.Errorf("unexpected config overrides status %d: %s", resp.StatusCode(), string(resp.Body)) } var config map[string]any - err = json.Unmarshal(contents, &config) + err = json.Unmarshal(resp.Body, &config) if err != nil { - return nil, errors.Errorf("failed to unmarshal response body: %w. Contents were %s", err, contents) + return nil, errors.Errorf("failed to unmarshal response body: %w", err) } return config, nil } diff --git a/internal/postgresConfig/get/get_test.go b/internal/postgresConfig/get/get_test.go new file mode 100644 index 0000000000..149c70758d --- /dev/null +++ b/internal/postgresConfig/get/get_test.go @@ -0,0 +1,85 @@ +package get + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" + "github.com/supabase/cli/pkg/cast" +) + +func TestPostgresConfig(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("get postgres config", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, ` + + Parameter | Value + -----------------|------- + max_connections | 100 + +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `max_connections = 100.0 +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected config overrides status 503:") + }) +} diff --git a/internal/postgresConfig/update/update.go b/internal/postgresConfig/update/update.go index 4d7e420870..ffb565ca06 100644 --- a/internal/postgresConfig/update/update.go +++ b/internal/postgresConfig/update/update.go @@ -67,13 +67,14 @@ func Run(ctx context.Context, projectRef string, values []string, replaceOverrid resp, err := utils.GetSupabase().V1UpdatePostgresConfigWithBodyWithResponse(ctx, projectRef, "application/json", bytes.NewReader(bts)) if err != nil { return errors.Errorf("failed to update config overrides: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected update config overrides status %d: %s", resp.StatusCode(), string(resp.Body)) } - if resp.JSON200 == nil { - if resp.StatusCode() == 400 { - return errors.Errorf("failed to update config overrides: %s (%s). This usually indicates that an unsupported or invalid config override was attempted. Please refer to https://supabase.com/docs/guides/platform/custom-postgres-config", resp.Status(), string(resp.Body)) - } - return errors.Errorf("failed to update config overrides: %s (%s)", resp.Status(), string(resp.Body)) + var config map[string]any + err = json.Unmarshal(resp.Body, &config) + if err != nil { + return errors.Errorf("failed to unmarshal update response: %w", err) } + return get.PrintOutPostgresConfigOverrides(config) } - return get.Run(ctx, projectRef, fsys) } diff --git a/internal/postgresConfig/update/update_test.go b/internal/postgresConfig/update/update_test.go new file mode 100644 index 0000000000..f8b65de9c6 --- /dev/null +++ b/internal/postgresConfig/update/update_test.go @@ -0,0 +1,95 @@ +package update + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" + "github.com/supabase/cli/pkg/cast" +) + +func TestUpdatePostgresConfig(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("updates postgres config", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, ` + + Parameter | Value + -----------------|------- + max_connections | 100 + +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Put("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{ + "max_connections=100", + "track_commit_timestamp=true", + "statement_timeout=600", + "wal_keep_size=1GB", + }, true, true, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on missing key", func(t *testing.T) { + err := Run(context.Background(), flags.ProjectRef, []string{"value"}, true, true, nil) + assert.ErrorContains(t, err, "expected config value in key:value format") + }) + + t.Run("throws error on missing project", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{}, false, false, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{}, false, false, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{}, true, true, nil) + assert.ErrorContains(t, err, "unexpected update config overrides status 503:") + }) +} diff --git a/internal/snippets/download/download.go b/internal/snippets/download/download.go index 22b45dd689..36b8deb18d 100644 --- a/internal/snippets/download/download.go +++ b/internal/snippets/download/download.go @@ -19,12 +19,9 @@ func Run(ctx context.Context, snippetId string, fsys afero.Fs) error { resp, err := utils.GetSupabase().V1GetASnippetWithResponse(ctx, id) if err != nil { return errors.Errorf("failed to download snippet: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected download snippet status %d: %s", resp.StatusCode(), string(resp.Body)) } - - if resp.JSON200 == nil { - return errors.New("Unexpected error downloading SQL snippet: " + string(resp.Body)) - } - fmt.Println(resp.JSON200.Content.Sql) return nil } diff --git a/internal/snippets/download/download_test.go b/internal/snippets/download/download_test.go new file mode 100644 index 0000000000..9a290ef4ad --- /dev/null +++ b/internal/snippets/download/download_test.go @@ -0,0 +1,73 @@ +package download + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/google/uuid" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestSnippetDownload(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + snippetId, err := uuid.NewUUID() + require.NoError(t, err) + + t.Run("downloads sql snippet", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, "select 1\n")) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/snippets/" + snippetId.String()). + Reply(http.StatusOK). + JSON(api.SnippetResponse{Content: struct { + Favorite *bool `json:"favorite,omitempty"` + SchemaVersion string `json:"schema_version"` + Sql string `json:"sql"` + }{ + Sql: "select 1", + }}) + // Run test + err = Run(context.Background(), snippetId.String(), nil) + assert.NoError(t, err) + }) + + t.Run("throws error on invalid id", func(t *testing.T) { + err := Run(context.Background(), "", nil) + assert.ErrorContains(t, err, "invalid snippet ID:") + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/snippets/" + snippetId.String()). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), snippetId.String(), nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/snippets/" + snippetId.String()). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), snippetId.String(), nil) + assert.ErrorContains(t, err, "unexpected download snippet status 503:") + }) +} diff --git a/internal/snippets/list/list.go b/internal/snippets/list/list.go index ca5529cee0..1c5c1ee8df 100644 --- a/internal/snippets/list/list.go +++ b/internal/snippets/list/list.go @@ -24,12 +24,12 @@ func Run(ctx context.Context, fsys afero.Fs) error { switch utils.OutputFormat.Value { case utils.OutputPretty: - table := `|ID|NAME|VISIBILITY|OWNER|CREATED AT (UTC)|UPDATED AT (UTC)| + var table strings.Builder + table.WriteString(`|ID|NAME|VISIBILITY|OWNER|CREATED AT (UTC)|UPDATED AT (UTC)| |-|-|-|-|-|-| -` +`) for _, snippet := range resp.JSON200.Data { - table += fmt.Sprintf( - "|`%s`|`%s`|`%s`|`%s`|`%s`|`%s`|\n", + fmt.Fprintf(&table, "|`%s`|`%s`|`%s`|`%s`|`%s`|`%s`|\n", snippet.Id, strings.ReplaceAll(snippet.Name, "|", "\\|"), strings.ReplaceAll(string(snippet.Visibility), "|", "\\|"), @@ -38,7 +38,7 @@ func Run(ctx context.Context, fsys afero.Fs) error { utils.FormatTimestamp(snippet.UpdatedAt), ) } - return utils.RenderTable(table) + return utils.RenderTable(table.String()) case utils.OutputEnv: return errors.New(utils.ErrEnvNotSupported) } diff --git a/internal/snippets/list/list_test.go b/internal/snippets/list/list_test.go new file mode 100644 index 0000000000..37848da493 --- /dev/null +++ b/internal/snippets/list/list_test.go @@ -0,0 +1,130 @@ +package list + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/oapi-codegen/nullable" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestListSnippets(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("lists sql snippets", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, ` + + ID | NAME | VISIBILITY | OWNER | CREATED AT (UTC) | UPDATED AT (UTC) + --------------|--------------|------------|----------|---------------------|--------------------- + test-snippet | Create table | user | supaseed | 2023-10-13 17:48:58 | 2023-10-13 17:48:58 + +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/snippets"). + Reply(http.StatusOK). + JSON(api.SnippetList{Data: []struct { + Description nullable.Nullable[string] `json:"description"` + Favorite bool `json:"favorite"` + Id string `json:"id"` + InsertedAt string `json:"inserted_at"` + Name string `json:"name"` + Owner struct { + Id float32 `json:"id"` + Username string `json:"username"` + } `json:"owner"` + Project struct { + Id float32 `json:"id"` + Name string `json:"name"` + } `json:"project"` + Type api.SnippetListDataType `json:"type"` + UpdatedAt string `json:"updated_at"` + UpdatedBy struct { + Id float32 `json:"id"` + Username string `json:"username"` + } `json:"updated_by"` + Visibility api.SnippetListDataVisibility `json:"visibility"` + }{{ + Id: "test-snippet", + Name: "Create table", + Visibility: api.SnippetListDataVisibilityUser, + Owner: struct { + Id float32 `json:"id"` + Username string `json:"username"` + }{ + Username: "supaseed", + }, + InsertedAt: "2023-10-13T17:48:58.491Z", + UpdatedAt: "2023-10-13T17:48:58.491Z", + }}}) + // Run test + err := Run(context.Background(), nil) + assert.NoError(t, err) + }) + + t.Run("encodes json output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputJson + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `{ + "data": null +} +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/snippets"). + Reply(http.StatusOK). + JSON(api.SnippetList{}) + // Run test + err := Run(context.Background(), nil) + assert.NoError(t, err) + }) + + t.Run("throws error on env format", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/snippets"). + Reply(http.StatusOK). + JSON(api.SnippetList{}) + // Run test + err := Run(context.Background(), nil) + assert.ErrorIs(t, err, utils.ErrEnvNotSupported) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/snippets"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/snippets"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), nil) + assert.ErrorContains(t, err, "unexpected list snippets status 503:") + }) +} diff --git a/internal/ssl_enforcement/get/get.go b/internal/ssl_enforcement/get/get.go index e6d58693ef..e16a6f29b4 100644 --- a/internal/ssl_enforcement/get/get.go +++ b/internal/ssl_enforcement/get/get.go @@ -3,29 +3,32 @@ package get import ( "context" "fmt" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/pkg/api" ) func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { - // 1. Sanity checks. - // 2. get ssl enforcement config - { - resp, err := utils.GetSupabase().V1GetSslEnforcementConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to retrieve SSL enforcement config: %w", err) - } - if resp.JSON200 == nil { - return errors.New("failed to retrieve SSL enforcement config; received: " + string(resp.Body)) - } + resp, err := utils.GetSupabase().V1GetSslEnforcementConfigWithResponse(ctx, projectRef) + if err != nil { + return errors.Errorf("failed to retrieve SSL enforcement config: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected SSL enforcement status %d: %s", resp.StatusCode(), string(resp.Body)) + } + if utils.OutputFormat.Value != utils.OutputPretty { + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON200) + } + return PrintSSLStatus(*resp.JSON200) +} - if resp.JSON200.CurrentConfig.Database && resp.JSON200.AppliedSuccessfully { - fmt.Println("SSL is being enforced.") - } else { - fmt.Println("SSL is *NOT* being enforced.") - } - return nil +func PrintSSLStatus(ssl api.SslEnforcementResponse) error { + if ssl.CurrentConfig.Database && ssl.AppliedSuccessfully { + fmt.Println("SSL is being enforced.") + } else { + fmt.Println("SSL is *NOT* being enforced.") } + return nil } diff --git a/internal/ssl_enforcement/get/get_test.go b/internal/ssl_enforcement/get/get_test.go new file mode 100644 index 0000000000..7ec99b9d7a --- /dev/null +++ b/internal/ssl_enforcement/get/get_test.go @@ -0,0 +1,82 @@ +package get + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestSSLEnforcement(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("get ssl enforcement", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, "SSL is *NOT* being enforced.\n")) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/ssl-enforcement"). + Reply(http.StatusOK). + JSON(api.SslEnforcementResponse{}) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("encodes env output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `APPLIEDSUCCESSFULLY="true" +CURRENTCONFIG_DATABASE="true" +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/ssl-enforcement"). + Reply(http.StatusOK). + JSON(api.SslEnforcementResponse{ + AppliedSuccessfully: true, + CurrentConfig: struct { + Database bool `json:"database"` + }{ + Database: true, + }, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/ssl-enforcement"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/ssl-enforcement"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected SSL enforcement status 503:") + }) +} diff --git a/internal/ssl_enforcement/update/update.go b/internal/ssl_enforcement/update/update.go index 8f1b5f56d9..7f0525136c 100644 --- a/internal/ssl_enforcement/update/update.go +++ b/internal/ssl_enforcement/update/update.go @@ -2,32 +2,26 @@ package update import ( "context" - "fmt" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" + "github.com/supabase/cli/internal/ssl_enforcement/get" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/api" ) func Run(ctx context.Context, projectRef string, enforceDbSsl bool, fsys afero.Fs) error { - // 1. sanity checks - // 2. update restrictions - { - body := api.V1UpdateSslEnforcementConfigJSONRequestBody{} - body.RequestedConfig.Database = enforceDbSsl - resp, err := utils.GetSupabase().V1UpdateSslEnforcementConfigWithResponse(ctx, projectRef, body) - if err != nil { - return errors.Errorf("failed to update ssl enforcement: %w", err) - } - if resp.JSON200 == nil { - return errors.New("failed to update SSL enforcement confnig: " + string(resp.Body)) - } - if resp.JSON200.CurrentConfig.Database && resp.JSON200.AppliedSuccessfully { - fmt.Println("SSL is now being enforced.") - } else { - fmt.Println("SSL is *NOT* being enforced.") - } - return nil + body := api.V1UpdateSslEnforcementConfigJSONRequestBody{} + body.RequestedConfig.Database = enforceDbSsl + resp, err := utils.GetSupabase().V1UpdateSslEnforcementConfigWithResponse(ctx, projectRef, body) + if err != nil { + return errors.Errorf("failed to update ssl enforcement: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected update SSL status %d: %s", resp.StatusCode(), string(resp.Body)) } + if utils.OutputFormat.Value != utils.OutputPretty { + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON200) + } + return get.PrintSSLStatus(*resp.JSON200) } diff --git a/internal/ssl_enforcement/update/update_test.go b/internal/ssl_enforcement/update/update_test.go new file mode 100644 index 0000000000..92a406a39c --- /dev/null +++ b/internal/ssl_enforcement/update/update_test.go @@ -0,0 +1,82 @@ +package update + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestUpdateSSL(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("enable ssl enforcement", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, "SSL is being enforced.\n")) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Put("v1/projects/" + flags.ProjectRef + "/ssl-enforcement"). + Reply(http.StatusOK). + JSON(api.SslEnforcementResponse{ + AppliedSuccessfully: true, + CurrentConfig: struct { + Database bool `json:"database"` + }{ + Database: true, + }, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, true, nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml format", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `APPLIEDSUCCESSFULLY="false" +CURRENTCONFIG_DATABASE="false" +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Put("v1/projects/" + flags.ProjectRef + "/ssl-enforcement"). + Reply(http.StatusOK). + JSON(api.SslEnforcementResponse{}) + // Run test + err := Run(context.Background(), flags.ProjectRef, false, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + flags.ProjectRef + "/ssl-enforcement"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, false, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + flags.ProjectRef + "/ssl-enforcement"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, false, nil) + assert.ErrorContains(t, err, "unexpected update SSL status 503:") + }) +} diff --git a/internal/start/start.go b/internal/start/start.go index c35efa2e36..53c0032de2 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -627,7 +627,9 @@ EOF if keys, err := json.Marshal(utils.Config.Auth.SigningKeys); err == nil { env = append(env, "GOTRUE_JWT_KEYS="+string(keys)) // TODO: deprecate HS256 when it's no longer supported + // TODO: remove VALIDMETHODS after a while to avoid breaking changes env = append(env, "GOTRUE_JWT_VALIDMETHODS=HS256,RS256,ES256") + env = append(env, "GOTRUE_JWT_VALID_METHODS=HS256,RS256,ES256") } if utils.Config.Auth.Email.Smtp != nil && utils.Config.Auth.Email.Smtp.Enabled { @@ -1166,6 +1168,9 @@ EOF "SUPABASE_SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value, "LOGFLARE_PRIVATE_ACCESS_TOKEN=" + utils.Config.Analytics.ApiKey, "OPENAI_API_KEY=" + utils.Config.Studio.OpenaiApiKey.Value, + "PGRST_DB_SCHEMAS=" + strings.Join(utils.Config.Api.Schemas, ","), + "PGRST_DB_EXTRA_SEARCH_PATH=" + strings.Join(utils.Config.Api.ExtraSearchPath, ","), + fmt.Sprintf("PGRST_DB_MAX_ROWS=%d", utils.Config.Api.MaxRows), fmt.Sprintf("LOGFLARE_URL=http://%v:4000", utils.LogflareId), fmt.Sprintf("NEXT_PUBLIC_ENABLE_LOGS=%v", utils.Config.Analytics.Enabled), fmt.Sprintf("NEXT_ANALYTICS_BACKEND_PROVIDER=%v", utils.Config.Analytics.Backend), @@ -1173,6 +1178,7 @@ EOF "SNIPPETS_MANAGEMENT_FOLDER=" + containerSnippetsPath, // Ref: https://github.com/vercel/next.js/issues/51684#issuecomment-1612834913 "HOSTNAME=0.0.0.0", + "POSTGRES_USER_READ_WRITE=postgres", }, Healthcheck: &container.HealthConfig{ Test: []string{"CMD-SHELL", `node --eval="fetch('http://127.0.0.1:3000/api/platform/profile').then((r) => {if (!r.ok) throw new Error(r.status)})"`}, diff --git a/internal/testing/apitest/platform.go b/internal/testing/apitest/platform.go index 54054108b4..6ae3d50ded 100644 --- a/internal/testing/apitest/platform.go +++ b/internal/testing/apitest/platform.go @@ -47,3 +47,14 @@ func AssertRequestsDone(t *testing.T) { fmt.Fprintln(os.Stderr, "Pending:", p.Request().Method, p.Request().URLStruct.Path) } } + +func MockPlatformAPI(t *testing.T) func() { + // Setup valid access token + token := RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + teardown := func() { + AssertRequestsDone(t) + gock.OffAll() + } + return teardown +} diff --git a/internal/testing/fstest/stdout.go b/internal/testing/fstest/stdout.go new file mode 100644 index 0000000000..06c7bb3dc8 --- /dev/null +++ b/internal/testing/fstest/stdout.go @@ -0,0 +1,26 @@ +package fstest + +import ( + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func MockStdout(t *testing.T, output string) func() { + r, w, err := os.Pipe() + require.NoError(t, err) + // Replace stdout + oldStdout := os.Stdout + teardown := func() { + os.Stdout = oldStdout + assert.NoError(t, w.Close()) + data, err := io.ReadAll(r) + assert.NoError(t, err) + assert.Equal(t, output, string(data)) + } + os.Stdout = w + return teardown +} diff --git a/internal/utils/connect.go b/internal/utils/connect.go index 3b52d29da5..eca6b410aa 100644 --- a/internal/utils/connect.go +++ b/internal/utils/connect.go @@ -44,6 +44,8 @@ func ToPostgresURL(config pgconn.Config) string { ) } +var ErrPrimaryNotFound = errors.New("primary database not found") + func GetPoolerConfigPrimary(ctx context.Context, ref string) (api.SupavisorConfigResponse, error) { var result api.SupavisorConfigResponse resp, err := GetSupabase().V1GetPoolerConfigWithResponse(ctx, ref) @@ -57,7 +59,7 @@ func GetPoolerConfigPrimary(ctx context.Context, ref string) (api.SupavisorConfi return config, nil } } - return result, errors.Errorf("primary database not found: %s", ref) + return result, errors.New(ErrPrimaryNotFound) } func GetPoolerConfig(projectRef string) *pgconn.Config { diff --git a/internal/utils/git.go b/internal/utils/git.go new file mode 100644 index 0000000000..2c2ad70337 --- /dev/null +++ b/internal/utils/git.go @@ -0,0 +1,26 @@ +package utils + +import ( + "os" + + "github.com/go-git/go-git/v5" + "github.com/spf13/afero" +) + +func GetGitBranch(fsys afero.Fs) string { + return GetGitBranchOrDefault("main", fsys) +} + +func GetGitBranchOrDefault(def string, fsys afero.Fs) string { + head := os.Getenv("GITHUB_HEAD_REF") + if len(head) > 0 { + return head + } + opts := &git.PlainOpenOptions{DetectDotGit: true} + if repo, err := git.PlainOpenWithOptions(".", opts); err == nil { + if ref, err := repo.Head(); err == nil { + return ref.Name().Short() + } + } + return def +} diff --git a/internal/utils/misc.go b/internal/utils/misc.go index b7e6315ff4..e08c991a61 100644 --- a/internal/utils/misc.go +++ b/internal/utils/misc.go @@ -75,7 +75,6 @@ var ( PoolerVersionPath = filepath.Join(TempDir, "pooler-version") RealtimeVersionPath = filepath.Join(TempDir, "realtime-version") CliVersionPath = filepath.Join(TempDir, "cli-latest") - ProfilePath = filepath.Join(TempDir, "profile") CurrBranchPath = filepath.Join(SupabaseDirPath, ".branches", "_current_branch") ClusterDir = filepath.Join(SupabaseDirPath, "cluster") SchemasDir = filepath.Join(SupabaseDirPath, "schemas") diff --git a/internal/utils/profile.go b/internal/utils/profile.go index 723b52fa87..e9495a481b 100644 --- a/internal/utils/profile.go +++ b/internal/utils/profile.go @@ -3,6 +3,8 @@ package utils import ( "context" "fmt" + "os" + "path/filepath" "sort" "strings" @@ -121,8 +123,11 @@ func getProfileName(fsys afero.Fs) string { if prof := viper.GetString("PROFILE"); viper.IsSet("PROFILE") { fmt.Fprintln(debuglogger, "Loading profile from flag:", prof) return prof - } else if content, err := afero.ReadFile(fsys, ProfilePath); err == nil { - fmt.Fprintln(debuglogger, "Loading profile from file:", ProfilePath) + } else if profilePath, err := getProfilePath(); err != nil { + fmt.Fprintln(debuglogger, err) + return prof + } else if content, err := afero.ReadFile(fsys, profilePath); err == nil { + fmt.Fprintln(debuglogger, "Loading profile from file:", profilePath) return string(content) } else { fmt.Fprintln(debuglogger, err) @@ -130,6 +135,22 @@ func getProfileName(fsys afero.Fs) string { } } +func getProfilePath() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", errors.Errorf("failed to get $HOME directory: %w", err) + } + return filepath.Join(home, ".supabase", "profile"), nil +} + +func SaveProfileName(prof string, fsys afero.Fs) error { + profilePath, err := getProfilePath() + if err != nil { + return err + } + return WriteFile(profilePath, []byte(prof), fsys) +} + func AwsRegions() []string { result := make([]string, len(allProfiles[0].ProjectRegions)) for i, region := range allProfiles[0].ProjectRegions { diff --git a/internal/vanity_subdomains/activate/activate.go b/internal/vanity_subdomains/activate/activate.go index 133ec7edd3..87eb748ea9 100644 --- a/internal/vanity_subdomains/activate/activate.go +++ b/internal/vanity_subdomains/activate/activate.go @@ -3,7 +3,7 @@ package activate import ( "context" "fmt" - "strings" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -12,26 +12,17 @@ import ( ) func Run(ctx context.Context, projectRef string, desiredSubdomain string, fsys afero.Fs) error { - // 1. Sanity checks. - subdomain := strings.TrimSpace(desiredSubdomain) - { - if len(subdomain) == 0 { - return errors.New("non-empty vanity subdomain expected") - } + resp, err := utils.GetSupabase().V1ActivateVanitySubdomainConfigWithResponse(ctx, projectRef, api.V1ActivateVanitySubdomainConfigJSONRequestBody{ + VanitySubdomain: desiredSubdomain, + }) + if err != nil { + return errors.Errorf("failed activate vanity subdomain: %w", err) + } else if resp.JSON201 == nil { + return errors.Errorf("unexpected activate vanity subdomain status %d: %s", resp.StatusCode(), string(resp.Body)) } - - // 2. create vanity subdomain - { - resp, err := utils.GetSupabase().V1ActivateVanitySubdomainConfigWithResponse(ctx, projectRef, api.V1ActivateVanitySubdomainConfigJSONRequestBody{ - VanitySubdomain: subdomain, - }) - if err != nil { - return errors.Errorf("failed activate vanity subdomain: %w", err) - } - if resp.JSON201 == nil { - return errors.New("failed to create vanity subdomain config: " + string(resp.Body)) - } - fmt.Printf("Activated vanity subdomain at %s\n", resp.JSON201.CustomDomain) - return nil + if utils.OutputFormat.Value != utils.OutputPretty { + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON201) } + fmt.Printf("Activated vanity subdomain at %s\n", resp.JSON201.CustomDomain) + return nil } diff --git a/internal/vanity_subdomains/activate/activate_test.go b/internal/vanity_subdomains/activate/activate_test.go new file mode 100644 index 0000000000..fb3ea7f84d --- /dev/null +++ b/internal/vanity_subdomains/activate/activate_test.go @@ -0,0 +1,76 @@ +package activate + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestActivateSubdomain(t *testing.T) { + t.Run("actives vanity subdomain", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, "Activated vanity subdomain at example.com\n")) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusCreated). + JSON(api.ActivateVanitySubdomainResponse{ + CustomDomain: "example.com", + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `CustomDomain = "example.com" +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusCreated). + JSON(api.ActivateVanitySubdomainResponse{ + CustomDomain: "example.com", + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.ErrorContains(t, err, "unexpected activate vanity subdomain status 503:") + }) +} diff --git a/internal/vanity_subdomains/check/check.go b/internal/vanity_subdomains/check/check.go index 3901227071..a9ecb639cc 100644 --- a/internal/vanity_subdomains/check/check.go +++ b/internal/vanity_subdomains/check/check.go @@ -3,7 +3,7 @@ package check import ( "context" "fmt" - "strings" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -12,26 +12,17 @@ import ( ) func Run(ctx context.Context, projectRef string, desiredSubdomain string, fsys afero.Fs) error { - // 1. Sanity checks. - subdomain := strings.TrimSpace(desiredSubdomain) - { - if len(subdomain) == 0 { - return errors.New("non-empty vanity subdomain expected") - } + resp, err := utils.GetSupabase().V1CheckVanitySubdomainAvailabilityWithResponse(ctx, projectRef, api.V1CheckVanitySubdomainAvailabilityJSONRequestBody{ + VanitySubdomain: desiredSubdomain, + }) + if err != nil { + return errors.Errorf("failed to check vanity subdomain: %w", err) + } else if resp.JSON201 == nil { + return errors.Errorf("unexpected check vanity subdomain status %d: %s", resp.StatusCode(), string(resp.Body)) } - - // 2. check if the subdomain is available - { - resp, err := utils.GetSupabase().V1CheckVanitySubdomainAvailabilityWithResponse(ctx, projectRef, api.V1CheckVanitySubdomainAvailabilityJSONRequestBody{ - VanitySubdomain: subdomain, - }) - if err != nil { - return errors.Errorf("failed to check vanity subdomain: %w", err) - } - if resp.JSON201 == nil { - return errors.New("failed to check subdomain availability: " + string(resp.Body)) - } - fmt.Printf("Subdomain %s available: %+v\n", subdomain, resp.JSON201.Available) - return nil + if utils.OutputFormat.Value != utils.OutputPretty { + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON201) } + fmt.Printf("Subdomain %s available: %+v\n", desiredSubdomain, resp.JSON201.Available) + return nil } diff --git a/internal/vanity_subdomains/check/check_test.go b/internal/vanity_subdomains/check/check_test.go new file mode 100644 index 0000000000..7702ab59c5 --- /dev/null +++ b/internal/vanity_subdomains/check/check_test.go @@ -0,0 +1,73 @@ +package check + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestCheckSubdomain(t *testing.T) { + t.Run("checks subdomain availability", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, "Subdomain example.com available: true\n")) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/vanity-subdomain/check-availability"). + Reply(http.StatusCreated). + JSON(api.SubdomainAvailabilityResponse{ + Available: true, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, "Available = false\n")) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/vanity-subdomain/check-availability"). + Reply(http.StatusCreated). + JSON(api.SubdomainAvailabilityResponse{}) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain/check-availability"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain/check-availability"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.ErrorContains(t, err, "unexpected check vanity subdomain status 503:") + }) +} diff --git a/internal/vanity_subdomains/delete/delete.go b/internal/vanity_subdomains/delete/delete.go index 3ee24d473d..969b63de7a 100644 --- a/internal/vanity_subdomains/delete/delete.go +++ b/internal/vanity_subdomains/delete/delete.go @@ -3,6 +3,8 @@ package delete import ( "context" "fmt" + "net/http" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -10,17 +12,12 @@ import ( ) func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { - // 1. Sanity checks. - // 2. delete config - { - resp, err := utils.GetSupabase().V1DeactivateVanitySubdomainConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to delete vanity subdomain: %w", err) - } - if resp.StatusCode() != 200 { - return errors.New("failed to delete vanity subdomain config; received: " + string(resp.Body)) - } - fmt.Println("Deleted vanity subdomain successfully.") - return nil + resp, err := utils.GetSupabase().V1DeactivateVanitySubdomainConfigWithResponse(ctx, projectRef) + if err != nil { + return errors.Errorf("failed to delete vanity subdomain: %w", err) + } else if resp.StatusCode() != http.StatusOK { + return errors.Errorf("unexpected delete vanity subdomain status %d: %s", resp.StatusCode(), string(resp.Body)) } + fmt.Fprintln(os.Stderr, "Deleted vanity subdomain successfully.") + return nil } diff --git a/internal/vanity_subdomains/delete/delete_test.go b/internal/vanity_subdomains/delete/delete_test.go new file mode 100644 index 0000000000..1a09295f60 --- /dev/null +++ b/internal/vanity_subdomains/delete/delete_test.go @@ -0,0 +1,54 @@ +package delete + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" +) + +func TestDeleteSubdomain(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("deletes vanity subdomain", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusOK) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected delete vanity subdomain status 503:") + }) +} diff --git a/internal/vanity_subdomains/get/get.go b/internal/vanity_subdomains/get/get.go index b466f18958..303e9edec1 100644 --- a/internal/vanity_subdomains/get/get.go +++ b/internal/vanity_subdomains/get/get.go @@ -3,6 +3,7 @@ package get import ( "context" "fmt" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -10,20 +11,18 @@ import ( ) func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { - // 1. Sanity checks. - // 2. get vanity subdomain config - { - response, err := utils.GetSupabase().V1GetVanitySubdomainConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to get vanity subdomain: %w", err) - } - if response.JSON200 == nil { - return errors.Errorf("failed to obtain vanity subdomain config: %+v", string(response.Body)) - } - fmt.Printf("Status: %s\n", response.JSON200.Status) - if response.JSON200.CustomDomain != nil { - fmt.Printf("Vanity subdomain: %s\n", *response.JSON200.CustomDomain) - } - return nil + resp, err := utils.GetSupabase().V1GetVanitySubdomainConfigWithResponse(ctx, projectRef) + if err != nil { + return errors.Errorf("failed to get vanity subdomain: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected vanity subdomain status %d: %s", resp.StatusCode(), string(resp.Body)) } + if utils.OutputFormat.Value != utils.OutputPretty { + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON200) + } + fmt.Printf("Status: %s\n", resp.JSON200.Status) + if resp.JSON200.CustomDomain != nil { + fmt.Printf("Vanity subdomain: %s\n", *resp.JSON200.CustomDomain) + } + return nil } diff --git a/internal/vanity_subdomains/get/get_test.go b/internal/vanity_subdomains/get/get_test.go new file mode 100644 index 0000000000..8ec0428f3b --- /dev/null +++ b/internal/vanity_subdomains/get/get_test.go @@ -0,0 +1,82 @@ +package get + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" + "github.com/supabase/cli/pkg/cast" +) + +func TestGetSubdomain(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("get vanity subdomains", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, `Status: custom-domain-used +Vanity subdomain: example.com +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusOK). + JSON(api.VanitySubdomainConfigResponse{ + CustomDomain: cast.Ptr("example.com"), + Status: api.CustomDomainUsed, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `Status = "not-used" +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusOK). + JSON(api.VanitySubdomainConfigResponse{ + Status: api.NotUsed, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected vanity subdomain status 503:") + }) +} diff --git a/internal/db/branch/create/create.go b/legacy/branch/create/create.go similarity index 100% rename from internal/db/branch/create/create.go rename to legacy/branch/create/create.go diff --git a/internal/db/branch/create/create_test.go b/legacy/branch/create/create_test.go similarity index 100% rename from internal/db/branch/create/create_test.go rename to legacy/branch/create/create_test.go diff --git a/internal/db/branch/create/templates/clone.sh b/legacy/branch/create/templates/clone.sh similarity index 100% rename from internal/db/branch/create/templates/clone.sh rename to legacy/branch/create/templates/clone.sh diff --git a/internal/db/branch/delete/delete.go b/legacy/branch/delete/delete.go similarity index 100% rename from internal/db/branch/delete/delete.go rename to legacy/branch/delete/delete.go diff --git a/internal/db/branch/delete/delete_test.go b/legacy/branch/delete/delete_test.go similarity index 100% rename from internal/db/branch/delete/delete_test.go rename to legacy/branch/delete/delete_test.go diff --git a/internal/db/branch/list/list.go b/legacy/branch/list/list.go similarity index 100% rename from internal/db/branch/list/list.go rename to legacy/branch/list/list.go diff --git a/internal/db/branch/list/list_test.go b/legacy/branch/list/list_test.go similarity index 100% rename from internal/db/branch/list/list_test.go rename to legacy/branch/list/list_test.go diff --git a/internal/db/branch/switch_/switch_.go b/legacy/branch/switch_/switch_.go similarity index 100% rename from internal/db/branch/switch_/switch_.go rename to legacy/branch/switch_/switch_.go diff --git a/internal/db/branch/switch_/switch__test.go b/legacy/branch/switch_/switch__test.go similarity index 100% rename from internal/db/branch/switch_/switch__test.go rename to legacy/branch/switch_/switch__test.go diff --git a/internal/db/diff/pgadmin.go b/legacy/diff/pgadmin.go similarity index 71% rename from internal/db/diff/pgadmin.go rename to legacy/diff/pgadmin.go index d53cd4660e..e6db262072 100644 --- a/internal/db/diff/pgadmin.go +++ b/legacy/diff/pgadmin.go @@ -4,34 +4,15 @@ import ( "context" _ "embed" "fmt" - "os" "github.com/jackc/pgconn" "github.com/spf13/afero" + "github.com/supabase/cli/internal/db/diff" "github.com/supabase/cli/internal/db/start" - "github.com/supabase/cli/internal/migration/new" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/config" ) -var warnDiff = `WARNING: The diff tool is not foolproof, so you may need to manually rearrange and modify the generated migration. -Run ` + utils.Aqua("supabase db reset") + ` to verify that the new migration does not generate errors.` - -func SaveDiff(out, file string, fsys afero.Fs) error { - if len(out) < 2 { - fmt.Fprintln(os.Stderr, "No schema changes found") - } else if len(file) > 0 { - path := new.GetMigrationPath(utils.GetCurrentTimestamp(), file) - if err := utils.WriteFile(path, []byte(out), fsys); err != nil { - return err - } - fmt.Fprintln(os.Stderr, warnDiff) - } else { - fmt.Println(out) - } - return nil -} - func RunPgAdmin(ctx context.Context, schema []string, file string, config pgconn.Config, fsys afero.Fs) error { // Sanity checks. if err := utils.AssertSupabaseDbIsRunning(); err != nil { @@ -44,7 +25,7 @@ func RunPgAdmin(ctx context.Context, schema []string, file string, config pgconn return err } - return SaveDiff(output, file, fsys) + return diff.SaveDiff(output, file, fsys) } var output string @@ -53,7 +34,7 @@ func run(p utils.Program, ctx context.Context, schema []string, config pgconn.Co p.Send(utils.StatusMsg("Creating shadow database...")) // 1. Create shadow db and run migrations - shadow, err := CreateShadowDatabase(ctx, utils.Config.Db.ShadowPort) + shadow, err := diff.CreateShadowDatabase(ctx, utils.Config.Db.ShadowPort) if err != nil { return err } @@ -61,7 +42,7 @@ func run(p utils.Program, ctx context.Context, schema []string, config pgconn.Co if err := start.WaitForHealthyService(ctx, utils.Config.Db.HealthTimeout, shadow); err != nil { return err } - if err := MigrateShadowDatabase(ctx, shadow, fsys); err != nil { + if err := diff.MigrateShadowDatabase(ctx, shadow, fsys); err != nil { return err } diff --git a/internal/gen/keys/keys.go b/legacy/keys/keys.go similarity index 83% rename from internal/gen/keys/keys.go rename to legacy/keys/keys.go index 6c81f4bd32..560a4ce917 100644 --- a/internal/gen/keys/keys.go +++ b/legacy/keys/keys.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/go-errors/errors" - "github.com/go-git/go-git/v5" "github.com/spf13/afero" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/config" @@ -24,7 +23,7 @@ type CustomName struct { } func Run(ctx context.Context, projectRef, format string, names CustomName, fsys afero.Fs) error { - branch := GetGitBranch(fsys) + branch := utils.GetGitBranch(fsys) if err := GenerateSecrets(ctx, projectRef, branch, fsys); err != nil { return err } @@ -74,21 +73,3 @@ func GenerateSecrets(ctx context.Context, projectRef, branch string, fsys afero. } return nil } - -func GetGitBranch(fsys afero.Fs) string { - return GetGitBranchOrDefault("main", fsys) -} - -func GetGitBranchOrDefault(def string, fsys afero.Fs) string { - head := os.Getenv("GITHUB_HEAD_REF") - if len(head) > 0 { - return head - } - opts := &git.PlainOpenOptions{DetectDotGit: true} - if repo, err := git.PlainOpenWithOptions(".", opts); err == nil { - if ref, err := repo.Head(); err == nil { - return ref.Name().Short() - } - } - return def -} diff --git a/package.json b/package.json index b89b2bfd98..bee0e8bb8b 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", - "tar": "7.5.7" + "tar": "7.5.11" }, "release": { "branches": [ diff --git a/pkg/api/client.gen.go b/pkg/api/client.gen.go index f6411a0c19..2a2672dc4a 100644 --- a/pkg/api/client.gen.go +++ b/pkg/api/client.gen.go @@ -372,6 +372,20 @@ type ClientInterface interface { V1UpdatePostgresConfig(ctx context.Context, ref string, body V1UpdatePostgresConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // V1GetDatabaseDisk request + V1GetDatabaseDisk(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // V1ModifyDatabaseDiskWithBody request with any body + V1ModifyDatabaseDiskWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + V1ModifyDatabaseDisk(ctx context.Context, ref string, body V1ModifyDatabaseDiskJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // V1GetProjectDiskAutoscaleConfig request + V1GetProjectDiskAutoscaleConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // V1GetDiskUtilization request + V1GetDiskUtilization(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) + // V1GetRealtimeConfig request V1GetRealtimeConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -380,6 +394,9 @@ type ClientInterface interface { V1UpdateRealtimeConfig(ctx context.Context, ref string, body V1UpdateRealtimeConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // V1ShutdownRealtime request + V1ShutdownRealtime(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) + // V1GetStorageConfig request V1GetStorageConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -523,6 +540,14 @@ type ClientInterface interface { // V1GetServicesHealth request V1GetServicesHealth(ctx context.Context, ref string, params *V1GetServicesHealthParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // V1GetJitAccessConfig request + V1GetJitAccessConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // V1UpdateJitAccessConfigWithBody request with any body + V1UpdateJitAccessConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + V1UpdateJitAccessConfig(ctx context.Context, ref string, body V1UpdateJitAccessConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // V1DeleteNetworkBansWithBody request with any body V1DeleteNetworkBansWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1866,6 +1891,66 @@ func (c *Client) V1UpdatePostgresConfig(ctx context.Context, ref string, body V1 return c.Client.Do(req) } +func (c *Client) V1GetDatabaseDisk(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewV1GetDatabaseDiskRequest(c.Server, ref) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) V1ModifyDatabaseDiskWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewV1ModifyDatabaseDiskRequestWithBody(c.Server, ref, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) V1ModifyDatabaseDisk(ctx context.Context, ref string, body V1ModifyDatabaseDiskJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewV1ModifyDatabaseDiskRequest(c.Server, ref, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) V1GetProjectDiskAutoscaleConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewV1GetProjectDiskAutoscaleConfigRequest(c.Server, ref) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) V1GetDiskUtilization(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewV1GetDiskUtilizationRequest(c.Server, ref) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) V1GetRealtimeConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewV1GetRealtimeConfigRequest(c.Server, ref) if err != nil { @@ -1902,6 +1987,18 @@ func (c *Client) V1UpdateRealtimeConfig(ctx context.Context, ref string, body V1 return c.Client.Do(req) } +func (c *Client) V1ShutdownRealtime(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewV1ShutdownRealtimeRequest(c.Server, ref) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) V1GetStorageConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewV1GetStorageConfigRequest(c.Server, ref) if err != nil { @@ -2538,6 +2635,42 @@ func (c *Client) V1GetServicesHealth(ctx context.Context, ref string, params *V1 return c.Client.Do(req) } +func (c *Client) V1GetJitAccessConfig(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewV1GetJitAccessConfigRequest(c.Server, ref) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) V1UpdateJitAccessConfigWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewV1UpdateJitAccessConfigRequestWithBody(c.Server, ref, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) V1UpdateJitAccessConfig(ctx context.Context, ref string, body V1UpdateJitAccessConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewV1UpdateJitAccessConfigRequest(c.Server, ref, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) V1DeleteNetworkBansWithBody(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewV1DeleteNetworkBansRequestWithBody(c.Server, ref, contentType, body) if err != nil { @@ -3284,6 +3417,22 @@ func NewV1DiffABranchRequest(server string, branchIdOrRef string, params *V1Diff } + if params.Pgdelta != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "pgdelta", runtime.ParamLocationQuery, *params.Pgdelta); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + queryURL.RawQuery = queryValues.Encode() } @@ -6896,8 +7045,8 @@ func NewV1UpdatePostgresConfigRequestWithBody(server string, ref string, content return req, nil } -// NewV1GetRealtimeConfigRequest generates requests for V1GetRealtimeConfig -func NewV1GetRealtimeConfigRequest(server string, ref string) (*http.Request, error) { +// NewV1GetDatabaseDiskRequest generates requests for V1GetDatabaseDisk +func NewV1GetDatabaseDiskRequest(server string, ref string) (*http.Request, error) { var err error var pathParam0 string @@ -6912,7 +7061,7 @@ func NewV1GetRealtimeConfigRequest(server string, ref string) (*http.Request, er return nil, err } - operationPath := fmt.Sprintf("/v1/projects/%s/config/realtime", pathParam0) + operationPath := fmt.Sprintf("/v1/projects/%s/config/disk", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -6930,19 +7079,19 @@ func NewV1GetRealtimeConfigRequest(server string, ref string) (*http.Request, er return req, nil } -// NewV1UpdateRealtimeConfigRequest calls the generic V1UpdateRealtimeConfig builder with application/json body -func NewV1UpdateRealtimeConfigRequest(server string, ref string, body V1UpdateRealtimeConfigJSONRequestBody) (*http.Request, error) { +// NewV1ModifyDatabaseDiskRequest calls the generic V1ModifyDatabaseDisk builder with application/json body +func NewV1ModifyDatabaseDiskRequest(server string, ref string, body V1ModifyDatabaseDiskJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader buf, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(buf) - return NewV1UpdateRealtimeConfigRequestWithBody(server, ref, "application/json", bodyReader) + return NewV1ModifyDatabaseDiskRequestWithBody(server, ref, "application/json", bodyReader) } -// NewV1UpdateRealtimeConfigRequestWithBody generates requests for V1UpdateRealtimeConfig with any type of body -func NewV1UpdateRealtimeConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { +// NewV1ModifyDatabaseDiskRequestWithBody generates requests for V1ModifyDatabaseDisk with any type of body +func NewV1ModifyDatabaseDiskRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { var err error var pathParam0 string @@ -6957,7 +7106,7 @@ func NewV1UpdateRealtimeConfigRequestWithBody(server string, ref string, content return nil, err } - operationPath := fmt.Sprintf("/v1/projects/%s/config/realtime", pathParam0) + operationPath := fmt.Sprintf("/v1/projects/%s/config/disk", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -6967,7 +7116,7 @@ func NewV1UpdateRealtimeConfigRequestWithBody(server string, ref string, content return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest("POST", queryURL.String(), body) if err != nil { return nil, err } @@ -6977,8 +7126,8 @@ func NewV1UpdateRealtimeConfigRequestWithBody(server string, ref string, content return req, nil } -// NewV1GetStorageConfigRequest generates requests for V1GetStorageConfig -func NewV1GetStorageConfigRequest(server string, ref string) (*http.Request, error) { +// NewV1GetProjectDiskAutoscaleConfigRequest generates requests for V1GetProjectDiskAutoscaleConfig +func NewV1GetProjectDiskAutoscaleConfigRequest(server string, ref string) (*http.Request, error) { var err error var pathParam0 string @@ -6993,7 +7142,7 @@ func NewV1GetStorageConfigRequest(server string, ref string) (*http.Request, err return nil, err } - operationPath := fmt.Sprintf("/v1/projects/%s/config/storage", pathParam0) + operationPath := fmt.Sprintf("/v1/projects/%s/config/disk/autoscale", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -7011,19 +7160,8 @@ func NewV1GetStorageConfigRequest(server string, ref string) (*http.Request, err return req, nil } -// NewV1UpdateStorageConfigRequest calls the generic V1UpdateStorageConfig builder with application/json body -func NewV1UpdateStorageConfigRequest(server string, ref string, body V1UpdateStorageConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateStorageConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateStorageConfigRequestWithBody generates requests for V1UpdateStorageConfig with any type of body -func NewV1UpdateStorageConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { +// NewV1GetDiskUtilizationRequest generates requests for V1GetDiskUtilization +func NewV1GetDiskUtilizationRequest(server string, ref string) (*http.Request, error) { var err error var pathParam0 string @@ -7038,7 +7176,7 @@ func NewV1UpdateStorageConfigRequestWithBody(server string, ref string, contentT return nil, err } - operationPath := fmt.Sprintf("/v1/projects/%s/config/storage", pathParam0) + operationPath := fmt.Sprintf("/v1/projects/%s/config/disk/util", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -7048,18 +7186,16 @@ func NewV1UpdateStorageConfigRequestWithBody(server string, ref string, contentT return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err } - req.Header.Add("Content-Type", contentType) - return req, nil } -// NewV1DeleteHostnameConfigRequest generates requests for V1DeleteHostnameConfig -func NewV1DeleteHostnameConfigRequest(server string, ref string) (*http.Request, error) { +// NewV1GetRealtimeConfigRequest generates requests for V1GetRealtimeConfig +func NewV1GetRealtimeConfigRequest(server string, ref string) (*http.Request, error) { var err error var pathParam0 string @@ -7074,7 +7210,7 @@ func NewV1DeleteHostnameConfigRequest(server string, ref string) (*http.Request, return nil, err } - operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname", pathParam0) + operationPath := fmt.Sprintf("/v1/projects/%s/config/realtime", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -7084,7 +7220,7 @@ func NewV1DeleteHostnameConfigRequest(server string, ref string) (*http.Request, return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err } @@ -7092,8 +7228,19 @@ func NewV1DeleteHostnameConfigRequest(server string, ref string) (*http.Request, return req, nil } -// NewV1GetHostnameConfigRequest generates requests for V1GetHostnameConfig -func NewV1GetHostnameConfigRequest(server string, ref string) (*http.Request, error) { +// NewV1UpdateRealtimeConfigRequest calls the generic V1UpdateRealtimeConfig builder with application/json body +func NewV1UpdateRealtimeConfigRequest(server string, ref string, body V1UpdateRealtimeConfigJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewV1UpdateRealtimeConfigRequestWithBody(server, ref, "application/json", bodyReader) +} + +// NewV1UpdateRealtimeConfigRequestWithBody generates requests for V1UpdateRealtimeConfig with any type of body +func NewV1UpdateRealtimeConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { var err error var pathParam0 string @@ -7108,7 +7255,7 @@ func NewV1GetHostnameConfigRequest(server string, ref string) (*http.Request, er return nil, err } - operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname", pathParam0) + operationPath := fmt.Sprintf("/v1/projects/%s/config/realtime", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -7118,16 +7265,18 @@ func NewV1GetHostnameConfigRequest(server string, ref string) (*http.Request, er return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest("PATCH", queryURL.String(), body) if err != nil { return nil, err } + req.Header.Add("Content-Type", contentType) + return req, nil } -// NewV1ActivateCustomHostnameRequest generates requests for V1ActivateCustomHostname -func NewV1ActivateCustomHostnameRequest(server string, ref string) (*http.Request, error) { +// NewV1ShutdownRealtimeRequest generates requests for V1ShutdownRealtime +func NewV1ShutdownRealtimeRequest(server string, ref string) (*http.Request, error) { var err error var pathParam0 string @@ -7142,7 +7291,7 @@ func NewV1ActivateCustomHostnameRequest(server string, ref string) (*http.Reques return nil, err } - operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname/activate", pathParam0) + operationPath := fmt.Sprintf("/v1/projects/%s/config/realtime/shutdown", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -7160,19 +7309,8 @@ func NewV1ActivateCustomHostnameRequest(server string, ref string) (*http.Reques return req, nil } -// NewV1UpdateHostnameConfigRequest calls the generic V1UpdateHostnameConfig builder with application/json body -func NewV1UpdateHostnameConfigRequest(server string, ref string, body V1UpdateHostnameConfigJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1UpdateHostnameConfigRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1UpdateHostnameConfigRequestWithBody generates requests for V1UpdateHostnameConfig with any type of body -func NewV1UpdateHostnameConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { +// NewV1GetStorageConfigRequest generates requests for V1GetStorageConfig +func NewV1GetStorageConfigRequest(server string, ref string) (*http.Request, error) { var err error var pathParam0 string @@ -7187,7 +7325,7 @@ func NewV1UpdateHostnameConfigRequestWithBody(server string, ref string, content return nil, err } - operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname/initialize", pathParam0) + operationPath := fmt.Sprintf("/v1/projects/%s/config/storage", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -7197,18 +7335,27 @@ func NewV1UpdateHostnameConfigRequestWithBody(server string, ref string, content return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err } - req.Header.Add("Content-Type", contentType) - return req, nil } -// NewV1VerifyDnsConfigRequest generates requests for V1VerifyDnsConfig -func NewV1VerifyDnsConfigRequest(server string, ref string) (*http.Request, error) { +// NewV1UpdateStorageConfigRequest calls the generic V1UpdateStorageConfig builder with application/json body +func NewV1UpdateStorageConfigRequest(server string, ref string, body V1UpdateStorageConfigJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewV1UpdateStorageConfigRequestWithBody(server, ref, "application/json", bodyReader) +} + +// NewV1UpdateStorageConfigRequestWithBody generates requests for V1UpdateStorageConfig with any type of body +func NewV1UpdateStorageConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { var err error var pathParam0 string @@ -7223,7 +7370,7 @@ func NewV1VerifyDnsConfigRequest(server string, ref string) (*http.Request, erro return nil, err } - operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname/reverify", pathParam0) + operationPath := fmt.Sprintf("/v1/projects/%s/config/storage", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -7233,16 +7380,18 @@ func NewV1VerifyDnsConfigRequest(server string, ref string) (*http.Request, erro return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest("PATCH", queryURL.String(), body) if err != nil { return nil, err } + req.Header.Add("Content-Type", contentType) + return req, nil } -// NewV1ListAllBackupsRequest generates requests for V1ListAllBackups -func NewV1ListAllBackupsRequest(server string, ref string) (*http.Request, error) { +// NewV1DeleteHostnameConfigRequest generates requests for V1DeleteHostnameConfig +func NewV1DeleteHostnameConfigRequest(server string, ref string) (*http.Request, error) { var err error var pathParam0 string @@ -7257,7 +7406,7 @@ func NewV1ListAllBackupsRequest(server string, ref string) (*http.Request, error return nil, err } - operationPath := fmt.Sprintf("/v1/projects/%s/database/backups", pathParam0) + operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -7267,7 +7416,7 @@ func NewV1ListAllBackupsRequest(server string, ref string) (*http.Request, error return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest("DELETE", queryURL.String(), nil) if err != nil { return nil, err } @@ -7275,19 +7424,8 @@ func NewV1ListAllBackupsRequest(server string, ref string) (*http.Request, error return req, nil } -// NewV1RestorePitrBackupRequest calls the generic V1RestorePitrBackup builder with application/json body -func NewV1RestorePitrBackupRequest(server string, ref string, body V1RestorePitrBackupJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewV1RestorePitrBackupRequestWithBody(server, ref, "application/json", bodyReader) -} - -// NewV1RestorePitrBackupRequestWithBody generates requests for V1RestorePitrBackup with any type of body -func NewV1RestorePitrBackupRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { +// NewV1GetHostnameConfigRequest generates requests for V1GetHostnameConfig +func NewV1GetHostnameConfigRequest(server string, ref string) (*http.Request, error) { var err error var pathParam0 string @@ -7302,7 +7440,7 @@ func NewV1RestorePitrBackupRequestWithBody(server string, ref string, contentTyp return nil, err } - operationPath := fmt.Sprintf("/v1/projects/%s/database/backups/restore-pitr", pathParam0) + operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -7312,18 +7450,16 @@ func NewV1RestorePitrBackupRequestWithBody(server string, ref string, contentTyp return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err } - req.Header.Add("Content-Type", contentType) - return req, nil } -// NewV1GetRestorePointRequest generates requests for V1GetRestorePoint -func NewV1GetRestorePointRequest(server string, ref string, params *V1GetRestorePointParams) (*http.Request, error) { +// NewV1ActivateCustomHostnameRequest generates requests for V1ActivateCustomHostname +func NewV1ActivateCustomHostnameRequest(server string, ref string) (*http.Request, error) { var err error var pathParam0 string @@ -7338,7 +7474,7 @@ func NewV1GetRestorePointRequest(server string, ref string, params *V1GetRestore return nil, err } - operationPath := fmt.Sprintf("/v1/projects/%s/database/backups/restore-point", pathParam0) + operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname/activate", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -7348,8 +7484,204 @@ func NewV1GetRestorePointRequest(server string, ref string, params *V1GetRestore return nil, err } - if params != nil { - queryValues := queryURL.Query() + req, err := http.NewRequest("POST", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewV1UpdateHostnameConfigRequest calls the generic V1UpdateHostnameConfig builder with application/json body +func NewV1UpdateHostnameConfigRequest(server string, ref string, body V1UpdateHostnameConfigJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewV1UpdateHostnameConfigRequestWithBody(server, ref, "application/json", bodyReader) +} + +// NewV1UpdateHostnameConfigRequestWithBody generates requests for V1UpdateHostnameConfig with any type of body +func NewV1UpdateHostnameConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname/initialize", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewV1VerifyDnsConfigRequest generates requests for V1VerifyDnsConfig +func NewV1VerifyDnsConfigRequest(server string, ref string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/custom-hostname/reverify", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewV1ListAllBackupsRequest generates requests for V1ListAllBackups +func NewV1ListAllBackupsRequest(server string, ref string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/database/backups", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewV1RestorePitrBackupRequest calls the generic V1RestorePitrBackup builder with application/json body +func NewV1RestorePitrBackupRequest(server string, ref string, body V1RestorePitrBackupJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewV1RestorePitrBackupRequestWithBody(server, ref, "application/json", bodyReader) +} + +// NewV1RestorePitrBackupRequestWithBody generates requests for V1RestorePitrBackup with any type of body +func NewV1RestorePitrBackupRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/database/backups/restore-pitr", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewV1GetRestorePointRequest generates requests for V1GetRestorePoint +func NewV1GetRestorePointRequest(server string, ref string, params *V1GetRestorePointParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/database/backups/restore-point", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() if params.Name != nil { @@ -8872,6 +9204,87 @@ func NewV1GetServicesHealthRequest(server string, ref string, params *V1GetServi return req, nil } +// NewV1GetJitAccessConfigRequest generates requests for V1GetJitAccessConfig +func NewV1GetJitAccessConfigRequest(server string, ref string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/jit-access", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewV1UpdateJitAccessConfigRequest calls the generic V1UpdateJitAccessConfig builder with application/json body +func NewV1UpdateJitAccessConfigRequest(server string, ref string, body V1UpdateJitAccessConfigJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewV1UpdateJitAccessConfigRequestWithBody(server, ref, "application/json", bodyReader) +} + +// NewV1UpdateJitAccessConfigRequestWithBody generates requests for V1UpdateJitAccessConfig with any type of body +func NewV1UpdateJitAccessConfigRequestWithBody(server string, ref string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/jit-access", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewV1DeleteNetworkBansRequest calls the generic V1DeleteNetworkBans builder with application/json body func NewV1DeleteNetworkBansRequest(server string, ref string, body V1DeleteNetworkBansJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -10645,6 +11058,20 @@ type ClientWithResponsesInterface interface { V1UpdatePostgresConfigWithResponse(ctx context.Context, ref string, body V1UpdatePostgresConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdatePostgresConfigResponse, error) + // V1GetDatabaseDiskWithResponse request + V1GetDatabaseDiskWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetDatabaseDiskResponse, error) + + // V1ModifyDatabaseDiskWithBodyWithResponse request with any body + V1ModifyDatabaseDiskWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ModifyDatabaseDiskResponse, error) + + V1ModifyDatabaseDiskWithResponse(ctx context.Context, ref string, body V1ModifyDatabaseDiskJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ModifyDatabaseDiskResponse, error) + + // V1GetProjectDiskAutoscaleConfigWithResponse request + V1GetProjectDiskAutoscaleConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectDiskAutoscaleConfigResponse, error) + + // V1GetDiskUtilizationWithResponse request + V1GetDiskUtilizationWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetDiskUtilizationResponse, error) + // V1GetRealtimeConfigWithResponse request V1GetRealtimeConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetRealtimeConfigResponse, error) @@ -10653,6 +11080,9 @@ type ClientWithResponsesInterface interface { V1UpdateRealtimeConfigWithResponse(ctx context.Context, ref string, body V1UpdateRealtimeConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateRealtimeConfigResponse, error) + // V1ShutdownRealtimeWithResponse request + V1ShutdownRealtimeWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ShutdownRealtimeResponse, error) + // V1GetStorageConfigWithResponse request V1GetStorageConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetStorageConfigResponse, error) @@ -10796,6 +11226,14 @@ type ClientWithResponsesInterface interface { // V1GetServicesHealthWithResponse request V1GetServicesHealthWithResponse(ctx context.Context, ref string, params *V1GetServicesHealthParams, reqEditors ...RequestEditorFn) (*V1GetServicesHealthResponse, error) + // V1GetJitAccessConfigWithResponse request + V1GetJitAccessConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetJitAccessConfigResponse, error) + + // V1UpdateJitAccessConfigWithBodyWithResponse request with any body + V1UpdateJitAccessConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateJitAccessConfigResponse, error) + + V1UpdateJitAccessConfigWithResponse(ctx context.Context, ref string, body V1UpdateJitAccessConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateJitAccessConfigResponse, error) + // V1DeleteNetworkBansWithBodyWithResponse request with any body V1DeleteNetworkBansWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1DeleteNetworkBansResponse, error) @@ -12520,14 +12958,124 @@ func (r V1GetProjectTpaIntegrationResponse) StatusCode() int { return 0 } -type V1GetProjectPgbouncerConfigResponse struct { +type V1GetProjectPgbouncerConfigResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *V1PgbouncerConfigResponse +} + +// Status returns HTTPResponse.Status +func (r V1GetProjectPgbouncerConfigResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r V1GetProjectPgbouncerConfigResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type V1GetPoolerConfigResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *[]SupavisorConfigResponse +} + +// Status returns HTTPResponse.Status +func (r V1GetPoolerConfigResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r V1GetPoolerConfigResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type V1UpdatePoolerConfigResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *UpdateSupavisorConfigResponse +} + +// Status returns HTTPResponse.Status +func (r V1UpdatePoolerConfigResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r V1UpdatePoolerConfigResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type V1GetPostgresConfigResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *PostgresConfigResponse +} + +// Status returns HTTPResponse.Status +func (r V1GetPostgresConfigResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r V1GetPostgresConfigResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type V1UpdatePostgresConfigResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *PostgresConfigResponse +} + +// Status returns HTTPResponse.Status +func (r V1UpdatePostgresConfigResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r V1UpdatePostgresConfigResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type V1GetDatabaseDiskResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *V1PgbouncerConfigResponse + JSON200 *DiskResponse } // Status returns HTTPResponse.Status -func (r V1GetProjectPgbouncerConfigResponse) Status() string { +func (r V1GetDatabaseDiskResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -12535,21 +13083,20 @@ func (r V1GetProjectPgbouncerConfigResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r V1GetProjectPgbouncerConfigResponse) StatusCode() int { +func (r V1GetDatabaseDiskResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type V1GetPoolerConfigResponse struct { +type V1ModifyDatabaseDiskResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *[]SupavisorConfigResponse } // Status returns HTTPResponse.Status -func (r V1GetPoolerConfigResponse) Status() string { +func (r V1ModifyDatabaseDiskResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -12557,21 +13104,21 @@ func (r V1GetPoolerConfigResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r V1GetPoolerConfigResponse) StatusCode() int { +func (r V1ModifyDatabaseDiskResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type V1UpdatePoolerConfigResponse struct { +type V1GetProjectDiskAutoscaleConfigResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *UpdateSupavisorConfigResponse + JSON200 *DiskAutoscaleConfig } // Status returns HTTPResponse.Status -func (r V1UpdatePoolerConfigResponse) Status() string { +func (r V1GetProjectDiskAutoscaleConfigResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -12579,21 +13126,21 @@ func (r V1UpdatePoolerConfigResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r V1UpdatePoolerConfigResponse) StatusCode() int { +func (r V1GetProjectDiskAutoscaleConfigResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type V1GetPostgresConfigResponse struct { +type V1GetDiskUtilizationResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *PostgresConfigResponse + JSON200 *DiskUtilMetricsResponse } // Status returns HTTPResponse.Status -func (r V1GetPostgresConfigResponse) Status() string { +func (r V1GetDiskUtilizationResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -12601,21 +13148,21 @@ func (r V1GetPostgresConfigResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r V1GetPostgresConfigResponse) StatusCode() int { +func (r V1GetDiskUtilizationResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type V1UpdatePostgresConfigResponse struct { +type V1GetRealtimeConfigResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *PostgresConfigResponse + JSON200 *RealtimeConfigResponse } // Status returns HTTPResponse.Status -func (r V1UpdatePostgresConfigResponse) Status() string { +func (r V1GetRealtimeConfigResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -12623,21 +13170,20 @@ func (r V1UpdatePostgresConfigResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r V1UpdatePostgresConfigResponse) StatusCode() int { +func (r V1GetRealtimeConfigResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type V1GetRealtimeConfigResponse struct { +type V1UpdateRealtimeConfigResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *RealtimeConfigResponse } // Status returns HTTPResponse.Status -func (r V1GetRealtimeConfigResponse) Status() string { +func (r V1UpdateRealtimeConfigResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -12645,20 +13191,20 @@ func (r V1GetRealtimeConfigResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r V1GetRealtimeConfigResponse) StatusCode() int { +func (r V1UpdateRealtimeConfigResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type V1UpdateRealtimeConfigResponse struct { +type V1ShutdownRealtimeResponse struct { Body []byte HTTPResponse *http.Response } // Status returns HTTPResponse.Status -func (r V1UpdateRealtimeConfigResponse) Status() string { +func (r V1ShutdownRealtimeResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -12666,7 +13212,7 @@ func (r V1UpdateRealtimeConfigResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r V1UpdateRealtimeConfigResponse) StatusCode() int { +func (r V1ShutdownRealtimeResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } @@ -13474,6 +14020,50 @@ func (r V1GetServicesHealthResponse) StatusCode() int { return 0 } +type V1GetJitAccessConfigResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *JitAccessResponse +} + +// Status returns HTTPResponse.Status +func (r V1GetJitAccessConfigResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r V1GetJitAccessConfigResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type V1UpdateJitAccessConfigResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *JitAccessResponse +} + +// Status returns HTTPResponse.Status +func (r V1UpdateJitAccessConfigResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r V1UpdateJitAccessConfigResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type V1DeleteNetworkBansResponse struct { Body []byte HTTPResponse *http.Response @@ -15100,6 +15690,50 @@ func (c *ClientWithResponses) V1UpdatePostgresConfigWithResponse(ctx context.Con return ParseV1UpdatePostgresConfigResponse(rsp) } +// V1GetDatabaseDiskWithResponse request returning *V1GetDatabaseDiskResponse +func (c *ClientWithResponses) V1GetDatabaseDiskWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetDatabaseDiskResponse, error) { + rsp, err := c.V1GetDatabaseDisk(ctx, ref, reqEditors...) + if err != nil { + return nil, err + } + return ParseV1GetDatabaseDiskResponse(rsp) +} + +// V1ModifyDatabaseDiskWithBodyWithResponse request with arbitrary body returning *V1ModifyDatabaseDiskResponse +func (c *ClientWithResponses) V1ModifyDatabaseDiskWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1ModifyDatabaseDiskResponse, error) { + rsp, err := c.V1ModifyDatabaseDiskWithBody(ctx, ref, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseV1ModifyDatabaseDiskResponse(rsp) +} + +func (c *ClientWithResponses) V1ModifyDatabaseDiskWithResponse(ctx context.Context, ref string, body V1ModifyDatabaseDiskJSONRequestBody, reqEditors ...RequestEditorFn) (*V1ModifyDatabaseDiskResponse, error) { + rsp, err := c.V1ModifyDatabaseDisk(ctx, ref, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseV1ModifyDatabaseDiskResponse(rsp) +} + +// V1GetProjectDiskAutoscaleConfigWithResponse request returning *V1GetProjectDiskAutoscaleConfigResponse +func (c *ClientWithResponses) V1GetProjectDiskAutoscaleConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetProjectDiskAutoscaleConfigResponse, error) { + rsp, err := c.V1GetProjectDiskAutoscaleConfig(ctx, ref, reqEditors...) + if err != nil { + return nil, err + } + return ParseV1GetProjectDiskAutoscaleConfigResponse(rsp) +} + +// V1GetDiskUtilizationWithResponse request returning *V1GetDiskUtilizationResponse +func (c *ClientWithResponses) V1GetDiskUtilizationWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetDiskUtilizationResponse, error) { + rsp, err := c.V1GetDiskUtilization(ctx, ref, reqEditors...) + if err != nil { + return nil, err + } + return ParseV1GetDiskUtilizationResponse(rsp) +} + // V1GetRealtimeConfigWithResponse request returning *V1GetRealtimeConfigResponse func (c *ClientWithResponses) V1GetRealtimeConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetRealtimeConfigResponse, error) { rsp, err := c.V1GetRealtimeConfig(ctx, ref, reqEditors...) @@ -15126,6 +15760,15 @@ func (c *ClientWithResponses) V1UpdateRealtimeConfigWithResponse(ctx context.Con return ParseV1UpdateRealtimeConfigResponse(rsp) } +// V1ShutdownRealtimeWithResponse request returning *V1ShutdownRealtimeResponse +func (c *ClientWithResponses) V1ShutdownRealtimeWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ShutdownRealtimeResponse, error) { + rsp, err := c.V1ShutdownRealtime(ctx, ref, reqEditors...) + if err != nil { + return nil, err + } + return ParseV1ShutdownRealtimeResponse(rsp) +} + // V1GetStorageConfigWithResponse request returning *V1GetStorageConfigResponse func (c *ClientWithResponses) V1GetStorageConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetStorageConfigResponse, error) { rsp, err := c.V1GetStorageConfig(ctx, ref, reqEditors...) @@ -15587,6 +16230,32 @@ func (c *ClientWithResponses) V1GetServicesHealthWithResponse(ctx context.Contex return ParseV1GetServicesHealthResponse(rsp) } +// V1GetJitAccessConfigWithResponse request returning *V1GetJitAccessConfigResponse +func (c *ClientWithResponses) V1GetJitAccessConfigWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1GetJitAccessConfigResponse, error) { + rsp, err := c.V1GetJitAccessConfig(ctx, ref, reqEditors...) + if err != nil { + return nil, err + } + return ParseV1GetJitAccessConfigResponse(rsp) +} + +// V1UpdateJitAccessConfigWithBodyWithResponse request with arbitrary body returning *V1UpdateJitAccessConfigResponse +func (c *ClientWithResponses) V1UpdateJitAccessConfigWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1UpdateJitAccessConfigResponse, error) { + rsp, err := c.V1UpdateJitAccessConfigWithBody(ctx, ref, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseV1UpdateJitAccessConfigResponse(rsp) +} + +func (c *ClientWithResponses) V1UpdateJitAccessConfigWithResponse(ctx context.Context, ref string, body V1UpdateJitAccessConfigJSONRequestBody, reqEditors ...RequestEditorFn) (*V1UpdateJitAccessConfigResponse, error) { + rsp, err := c.V1UpdateJitAccessConfig(ctx, ref, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseV1UpdateJitAccessConfigResponse(rsp) +} + // V1DeleteNetworkBansWithBodyWithResponse request with arbitrary body returning *V1DeleteNetworkBansResponse func (c *ClientWithResponses) V1DeleteNetworkBansWithBodyWithResponse(ctx context.Context, ref string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1DeleteNetworkBansResponse, error) { rsp, err := c.V1DeleteNetworkBansWithBody(ctx, ref, contentType, body, reqEditors...) @@ -17915,6 +18584,100 @@ func ParseV1UpdatePostgresConfigResponse(rsp *http.Response) (*V1UpdatePostgresC return response, nil } +// ParseV1GetDatabaseDiskResponse parses an HTTP response from a V1GetDatabaseDiskWithResponse call +func ParseV1GetDatabaseDiskResponse(rsp *http.Response) (*V1GetDatabaseDiskResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &V1GetDatabaseDiskResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DiskResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseV1ModifyDatabaseDiskResponse parses an HTTP response from a V1ModifyDatabaseDiskWithResponse call +func ParseV1ModifyDatabaseDiskResponse(rsp *http.Response) (*V1ModifyDatabaseDiskResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &V1ModifyDatabaseDiskResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseV1GetProjectDiskAutoscaleConfigResponse parses an HTTP response from a V1GetProjectDiskAutoscaleConfigWithResponse call +func ParseV1GetProjectDiskAutoscaleConfigResponse(rsp *http.Response) (*V1GetProjectDiskAutoscaleConfigResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &V1GetProjectDiskAutoscaleConfigResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DiskAutoscaleConfig + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseV1GetDiskUtilizationResponse parses an HTTP response from a V1GetDiskUtilizationWithResponse call +func ParseV1GetDiskUtilizationResponse(rsp *http.Response) (*V1GetDiskUtilizationResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &V1GetDiskUtilizationResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DiskUtilMetricsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + // ParseV1GetRealtimeConfigResponse parses an HTTP response from a V1GetRealtimeConfigWithResponse call func ParseV1GetRealtimeConfigResponse(rsp *http.Response) (*V1GetRealtimeConfigResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -17957,6 +18720,22 @@ func ParseV1UpdateRealtimeConfigResponse(rsp *http.Response) (*V1UpdateRealtimeC return response, nil } +// ParseV1ShutdownRealtimeResponse parses an HTTP response from a V1ShutdownRealtimeWithResponse call +func ParseV1ShutdownRealtimeResponse(rsp *http.Response) (*V1ShutdownRealtimeResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &V1ShutdownRealtimeResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + // ParseV1GetStorageConfigResponse parses an HTTP response from a V1GetStorageConfigWithResponse call func ParseV1GetStorageConfigResponse(rsp *http.Response) (*V1GetStorageConfigResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -18789,6 +19568,58 @@ func ParseV1GetServicesHealthResponse(rsp *http.Response) (*V1GetServicesHealthR return response, nil } +// ParseV1GetJitAccessConfigResponse parses an HTTP response from a V1GetJitAccessConfigWithResponse call +func ParseV1GetJitAccessConfigResponse(rsp *http.Response) (*V1GetJitAccessConfigResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &V1GetJitAccessConfigResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest JitAccessResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseV1UpdateJitAccessConfigResponse parses an HTTP response from a V1UpdateJitAccessConfigWithResponse call +func ParseV1UpdateJitAccessConfigResponse(rsp *http.Response) (*V1UpdateJitAccessConfigResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &V1UpdateJitAccessConfigResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest JitAccessResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + // ParseV1DeleteNetworkBansResponse parses an HTTP response from a V1DeleteNetworkBansWithResponse call func ParseV1DeleteNetworkBansResponse(rsp *http.Response) (*V1DeleteNetworkBansResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index 65079455c9..1e95e93da7 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -437,6 +437,26 @@ const ( DeployFunctionResponseStatusTHROTTLED DeployFunctionResponseStatus = "THROTTLED" ) +// Defines values for DiskRequestBodyAttributes0Type. +const ( + DiskRequestBodyAttributes0TypeGp3 DiskRequestBodyAttributes0Type = "gp3" +) + +// Defines values for DiskRequestBodyAttributes1Type. +const ( + DiskRequestBodyAttributes1TypeIo2 DiskRequestBodyAttributes1Type = "io2" +) + +// Defines values for DiskResponseAttributes0Type. +const ( + DiskResponseAttributes0TypeGp3 DiskResponseAttributes0Type = "gp3" +) + +// Defines values for DiskResponseAttributes1Type. +const ( + DiskResponseAttributes1TypeIo2 DiskResponseAttributes1Type = "io2" +) + // Defines values for FunctionResponseStatus. const ( FunctionResponseStatusACTIVE FunctionResponseStatus = "ACTIVE" @@ -470,6 +490,13 @@ const ( GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannelWithdrawn GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannel = "withdrawn" ) +// Defines values for JitAccessRequestRequestState. +const ( + Disabled JitAccessRequestRequestState = "disabled" + Enabled JitAccessRequestRequestState = "enabled" + Unavailable JitAccessRequestRequestState = "unavailable" +) + // Defines values for ListActionRunResponseRunStepsName. const ( ListActionRunResponseRunStepsNameClone ListActionRunResponseRunStepsName = "clone" @@ -801,11 +828,11 @@ const ( // Defines values for ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion. const ( - ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersionN13 ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion = "13" - ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersionN14 ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion = "14" - ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersionN15 ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion = "15" - ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersionN17 ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion = "17" - ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersionN17Oriole ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion = "17-oriole" + N13 ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion = "13" + N14 ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion = "14" + N15 ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion = "15" + N17 ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion = "17" + N17Oriole ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion = "17-oriole" ) // Defines values for ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannel. @@ -876,6 +903,28 @@ const ( RegionsInfoAllSmartGroupTypeSmartGroup RegionsInfoAllSmartGroupType = "smartGroup" ) +// Defines values for RegionsInfoAllSpecificCode. +const ( + RegionsInfoAllSpecificCodeApEast1 RegionsInfoAllSpecificCode = "ap-east-1" + RegionsInfoAllSpecificCodeApNortheast1 RegionsInfoAllSpecificCode = "ap-northeast-1" + RegionsInfoAllSpecificCodeApNortheast2 RegionsInfoAllSpecificCode = "ap-northeast-2" + RegionsInfoAllSpecificCodeApSouth1 RegionsInfoAllSpecificCode = "ap-south-1" + RegionsInfoAllSpecificCodeApSoutheast1 RegionsInfoAllSpecificCode = "ap-southeast-1" + RegionsInfoAllSpecificCodeApSoutheast2 RegionsInfoAllSpecificCode = "ap-southeast-2" + RegionsInfoAllSpecificCodeCaCentral1 RegionsInfoAllSpecificCode = "ca-central-1" + RegionsInfoAllSpecificCodeEuCentral1 RegionsInfoAllSpecificCode = "eu-central-1" + RegionsInfoAllSpecificCodeEuCentral2 RegionsInfoAllSpecificCode = "eu-central-2" + RegionsInfoAllSpecificCodeEuNorth1 RegionsInfoAllSpecificCode = "eu-north-1" + RegionsInfoAllSpecificCodeEuWest1 RegionsInfoAllSpecificCode = "eu-west-1" + RegionsInfoAllSpecificCodeEuWest2 RegionsInfoAllSpecificCode = "eu-west-2" + RegionsInfoAllSpecificCodeEuWest3 RegionsInfoAllSpecificCode = "eu-west-3" + RegionsInfoAllSpecificCodeSaEast1 RegionsInfoAllSpecificCode = "sa-east-1" + RegionsInfoAllSpecificCodeUsEast1 RegionsInfoAllSpecificCode = "us-east-1" + RegionsInfoAllSpecificCodeUsEast2 RegionsInfoAllSpecificCode = "us-east-2" + RegionsInfoAllSpecificCodeUsWest1 RegionsInfoAllSpecificCode = "us-west-1" + RegionsInfoAllSpecificCodeUsWest2 RegionsInfoAllSpecificCode = "us-west-2" +) + // Defines values for RegionsInfoAllSpecificProvider. const ( RegionsInfoAllSpecificProviderAWS RegionsInfoAllSpecificProvider = "AWS" @@ -907,6 +956,28 @@ const ( RegionsInfoRecommendationsSmartGroupTypeSmartGroup RegionsInfoRecommendationsSmartGroupType = "smartGroup" ) +// Defines values for RegionsInfoRecommendationsSpecificCode. +const ( + RegionsInfoRecommendationsSpecificCodeApEast1 RegionsInfoRecommendationsSpecificCode = "ap-east-1" + RegionsInfoRecommendationsSpecificCodeApNortheast1 RegionsInfoRecommendationsSpecificCode = "ap-northeast-1" + RegionsInfoRecommendationsSpecificCodeApNortheast2 RegionsInfoRecommendationsSpecificCode = "ap-northeast-2" + RegionsInfoRecommendationsSpecificCodeApSouth1 RegionsInfoRecommendationsSpecificCode = "ap-south-1" + RegionsInfoRecommendationsSpecificCodeApSoutheast1 RegionsInfoRecommendationsSpecificCode = "ap-southeast-1" + RegionsInfoRecommendationsSpecificCodeApSoutheast2 RegionsInfoRecommendationsSpecificCode = "ap-southeast-2" + RegionsInfoRecommendationsSpecificCodeCaCentral1 RegionsInfoRecommendationsSpecificCode = "ca-central-1" + RegionsInfoRecommendationsSpecificCodeEuCentral1 RegionsInfoRecommendationsSpecificCode = "eu-central-1" + RegionsInfoRecommendationsSpecificCodeEuCentral2 RegionsInfoRecommendationsSpecificCode = "eu-central-2" + RegionsInfoRecommendationsSpecificCodeEuNorth1 RegionsInfoRecommendationsSpecificCode = "eu-north-1" + RegionsInfoRecommendationsSpecificCodeEuWest1 RegionsInfoRecommendationsSpecificCode = "eu-west-1" + RegionsInfoRecommendationsSpecificCodeEuWest2 RegionsInfoRecommendationsSpecificCode = "eu-west-2" + RegionsInfoRecommendationsSpecificCodeEuWest3 RegionsInfoRecommendationsSpecificCode = "eu-west-3" + RegionsInfoRecommendationsSpecificCodeSaEast1 RegionsInfoRecommendationsSpecificCode = "sa-east-1" + RegionsInfoRecommendationsSpecificCodeUsEast1 RegionsInfoRecommendationsSpecificCode = "us-east-1" + RegionsInfoRecommendationsSpecificCodeUsEast2 RegionsInfoRecommendationsSpecificCode = "us-east-2" + RegionsInfoRecommendationsSpecificCodeUsWest1 RegionsInfoRecommendationsSpecificCode = "us-west-1" + RegionsInfoRecommendationsSpecificCodeUsWest2 RegionsInfoRecommendationsSpecificCode = "us-west-2" +) + // Defines values for RegionsInfoRecommendationsSpecificProvider. const ( RegionsInfoRecommendationsSpecificProviderAWS RegionsInfoRecommendationsSpecificProvider = "AWS" @@ -1228,7 +1299,6 @@ const ( V1CreateProjectBodyDesiredInstanceSizeN4xlarge V1CreateProjectBodyDesiredInstanceSize = "4xlarge" V1CreateProjectBodyDesiredInstanceSizeN8xlarge V1CreateProjectBodyDesiredInstanceSize = "8xlarge" V1CreateProjectBodyDesiredInstanceSizeNano V1CreateProjectBodyDesiredInstanceSize = "nano" - V1CreateProjectBodyDesiredInstanceSizePico V1CreateProjectBodyDesiredInstanceSize = "pico" V1CreateProjectBodyDesiredInstanceSizeSmall V1CreateProjectBodyDesiredInstanceSize = "small" V1CreateProjectBodyDesiredInstanceSizeXlarge V1CreateProjectBodyDesiredInstanceSize = "xlarge" ) @@ -1239,13 +1309,6 @@ const ( V1CreateProjectBodyPlanPro V1CreateProjectBodyPlan = "pro" ) -// Defines values for V1CreateProjectBodyPostgresEngine. -const ( - V1CreateProjectBodyPostgresEngineN15 V1CreateProjectBodyPostgresEngine = "15" - V1CreateProjectBodyPostgresEngineN17 V1CreateProjectBodyPostgresEngine = "17" - V1CreateProjectBodyPostgresEngineN17Oriole V1CreateProjectBodyPostgresEngine = "17-oriole" -) - // Defines values for V1CreateProjectBodyRegion. const ( V1CreateProjectBodyRegionApEast1 V1CreateProjectBodyRegion = "ap-east-1" @@ -1307,24 +1370,14 @@ const ( SmartGroup V1CreateProjectBodyRegionSelection1Type = "smartGroup" ) -// Defines values for V1CreateProjectBodyReleaseChannel. -const ( - V1CreateProjectBodyReleaseChannelAlpha V1CreateProjectBodyReleaseChannel = "alpha" - V1CreateProjectBodyReleaseChannelBeta V1CreateProjectBodyReleaseChannel = "beta" - V1CreateProjectBodyReleaseChannelGa V1CreateProjectBodyReleaseChannel = "ga" - V1CreateProjectBodyReleaseChannelInternal V1CreateProjectBodyReleaseChannel = "internal" - V1CreateProjectBodyReleaseChannelPreview V1CreateProjectBodyReleaseChannel = "preview" - V1CreateProjectBodyReleaseChannelWithdrawn V1CreateProjectBodyReleaseChannel = "withdrawn" -) - // Defines values for V1OrganizationSlugResponseAllowedReleaseChannels. const ( - Alpha V1OrganizationSlugResponseAllowedReleaseChannels = "alpha" - Beta V1OrganizationSlugResponseAllowedReleaseChannels = "beta" - Ga V1OrganizationSlugResponseAllowedReleaseChannels = "ga" - Internal V1OrganizationSlugResponseAllowedReleaseChannels = "internal" - Preview V1OrganizationSlugResponseAllowedReleaseChannels = "preview" - Withdrawn V1OrganizationSlugResponseAllowedReleaseChannels = "withdrawn" + V1OrganizationSlugResponseAllowedReleaseChannelsAlpha V1OrganizationSlugResponseAllowedReleaseChannels = "alpha" + V1OrganizationSlugResponseAllowedReleaseChannelsBeta V1OrganizationSlugResponseAllowedReleaseChannels = "beta" + V1OrganizationSlugResponseAllowedReleaseChannelsGa V1OrganizationSlugResponseAllowedReleaseChannels = "ga" + V1OrganizationSlugResponseAllowedReleaseChannelsInternal V1OrganizationSlugResponseAllowedReleaseChannels = "internal" + V1OrganizationSlugResponseAllowedReleaseChannelsPreview V1OrganizationSlugResponseAllowedReleaseChannels = "preview" + V1OrganizationSlugResponseAllowedReleaseChannelsWithdrawn V1OrganizationSlugResponseAllowedReleaseChannels = "withdrawn" ) // Defines values for V1OrganizationSlugResponseOptInTags. @@ -1554,7 +1607,6 @@ const ( V1GetAvailableRegionsParamsDesiredInstanceSizeN4xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "4xlarge" V1GetAvailableRegionsParamsDesiredInstanceSizeN8xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "8xlarge" V1GetAvailableRegionsParamsDesiredInstanceSizeNano V1GetAvailableRegionsParamsDesiredInstanceSize = "nano" - V1GetAvailableRegionsParamsDesiredInstanceSizePico V1GetAvailableRegionsParamsDesiredInstanceSize = "pico" V1GetAvailableRegionsParamsDesiredInstanceSizeSmall V1GetAvailableRegionsParamsDesiredInstanceSize = "small" V1GetAvailableRegionsParamsDesiredInstanceSizeXlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "xlarge" ) @@ -1748,6 +1800,8 @@ type ApplyProjectAddonBody_AddonVariant struct { // AuthConfigResponse defines model for AuthConfigResponse. type AuthConfigResponse struct { ApiMaxRequestDuration nullable.Nullable[int] `json:"api_max_request_duration"` + CustomOauthEnabled bool `json:"custom_oauth_enabled"` + CustomOauthMaxProviders int `json:"custom_oauth_max_providers"` DbMaxPoolSize nullable.Nullable[int] `json:"db_max_pool_size"` DbMaxPoolSizeUnit nullable.Nullable[AuthConfigResponseDbMaxPoolSizeUnit] `json:"db_max_pool_size_unit"` DisableSignup nullable.Nullable[bool] `json:"disable_signup"` @@ -1941,6 +1995,7 @@ type AuthConfigResponse struct { SecurityCaptchaSecret nullable.Nullable[string] `json:"security_captcha_secret"` SecurityManualLinkingEnabled nullable.Nullable[bool] `json:"security_manual_linking_enabled"` SecurityRefreshTokenReuseInterval nullable.Nullable[int] `json:"security_refresh_token_reuse_interval"` + SecuritySbForwardedForEnabled nullable.Nullable[bool] `json:"security_sb_forwarded_for_enabled"` SecurityUpdatePasswordRequireReauthentication nullable.Nullable[bool] `json:"security_update_password_require_reauthentication"` SessionsInactivityTimeout nullable.Nullable[int] `json:"sessions_inactivity_timeout"` SessionsSinglePerUser nullable.Nullable[bool] `json:"sessions_single_per_user"` @@ -2466,6 +2521,91 @@ type DeployFunctionResponse struct { // DeployFunctionResponseStatus defines model for DeployFunctionResponse.Status. type DeployFunctionResponseStatus string +// DiskAutoscaleConfig defines model for DiskAutoscaleConfig. +type DiskAutoscaleConfig struct { + // GrowthPercent Growth percentage for disk autoscaling + GrowthPercent nullable.Nullable[int] `json:"growth_percent"` + + // MaxSizeGb Maximum limit the disk size will grow to in GB + MaxSizeGb nullable.Nullable[int] `json:"max_size_gb"` + + // MinIncrementGb Minimum increment size for disk autoscaling in GB + MinIncrementGb nullable.Nullable[int] `json:"min_increment_gb"` +} + +// DiskRequestBody defines model for DiskRequestBody. +type DiskRequestBody struct { + Attributes DiskRequestBody_Attributes `json:"attributes"` +} + +// DiskRequestBodyAttributes0 defines model for . +type DiskRequestBodyAttributes0 struct { + Iops int `json:"iops"` + SizeGb int `json:"size_gb"` + ThroughputMibps *int `json:"throughput_mibps,omitempty"` + Type DiskRequestBodyAttributes0Type `json:"type"` +} + +// DiskRequestBodyAttributes0Type defines model for DiskRequestBody.Attributes.0.Type. +type DiskRequestBodyAttributes0Type string + +// DiskRequestBodyAttributes1 defines model for . +type DiskRequestBodyAttributes1 struct { + Iops int `json:"iops"` + SizeGb int `json:"size_gb"` + Type DiskRequestBodyAttributes1Type `json:"type"` +} + +// DiskRequestBodyAttributes1Type defines model for DiskRequestBody.Attributes.1.Type. +type DiskRequestBodyAttributes1Type string + +// DiskRequestBody_Attributes defines model for DiskRequestBody.Attributes. +type DiskRequestBody_Attributes struct { + union json.RawMessage +} + +// DiskResponse defines model for DiskResponse. +type DiskResponse struct { + Attributes DiskResponse_Attributes `json:"attributes"` + LastModifiedAt *string `json:"last_modified_at,omitempty"` +} + +// DiskResponseAttributes0 defines model for . +type DiskResponseAttributes0 struct { + Iops int `json:"iops"` + SizeGb int `json:"size_gb"` + ThroughputMibps *int `json:"throughput_mibps,omitempty"` + Type DiskResponseAttributes0Type `json:"type"` +} + +// DiskResponseAttributes0Type defines model for DiskResponse.Attributes.0.Type. +type DiskResponseAttributes0Type string + +// DiskResponseAttributes1 defines model for . +type DiskResponseAttributes1 struct { + Iops int `json:"iops"` + SizeGb int `json:"size_gb"` + Type DiskResponseAttributes1Type `json:"type"` +} + +// DiskResponseAttributes1Type defines model for DiskResponse.Attributes.1.Type. +type DiskResponseAttributes1Type string + +// DiskResponse_Attributes defines model for DiskResponse.Attributes. +type DiskResponse_Attributes struct { + union json.RawMessage +} + +// DiskUtilMetricsResponse defines model for DiskUtilMetricsResponse. +type DiskUtilMetricsResponse struct { + Metrics struct { + FsAvailBytes float32 `json:"fs_avail_bytes"` + FsSizeBytes float32 `json:"fs_size_bytes"` + FsUsedBytes float32 `json:"fs_used_bytes"` + } `json:"metrics"` + Timestamp string `json:"timestamp"` +} + // FunctionDeployBody defines model for FunctionDeployBody. type FunctionDeployBody struct { File *[]openapi_types.File `json:"file,omitempty"` @@ -2577,6 +2717,14 @@ type GetProviderResponse struct { UpdatedAt *string `json:"updated_at,omitempty"` } +// JitAccessRequestRequest defines model for JitAccessRequestRequest. +type JitAccessRequestRequest struct { + State JitAccessRequestRequestState `json:"state"` +} + +// JitAccessRequestRequestState defines model for JitAccessRequestRequest.State. +type JitAccessRequestRequestState string + // JitAccessResponse defines model for JitAccessResponse. type JitAccessResponse struct { UserId openapi_types.UUID `json:"user_id"` @@ -3249,10 +3397,13 @@ type RealtimeConfigResponse struct { // MaxPresenceEventsPerSecond Sets maximum number of presence events per second rate limit MaxPresenceEventsPerSecond nullable.Nullable[int] `json:"max_presence_events_per_second"` + // PresenceEnabled Whether to enable presence + PresenceEnabled bool `json:"presence_enabled"` + // PrivateOnly Whether to only allow private channels PrivateOnly nullable.Nullable[bool] `json:"private_only"` - // Suspend Whether to suspend realtime + // Suspend Disables the Realtime service for this project when true. Set to false to re-enable it. Suspend nullable.Nullable[bool] `json:"suspend"` } @@ -3265,7 +3416,7 @@ type RegionsInfo struct { Type RegionsInfoAllSmartGroupType `json:"type"` } `json:"smartGroup"` Specific []struct { - Code string `json:"code"` + Code RegionsInfoAllSpecificCode `json:"code"` Name string `json:"name"` Provider RegionsInfoAllSpecificProvider `json:"provider"` Status *RegionsInfoAllSpecificStatus `json:"status,omitempty"` @@ -3279,7 +3430,7 @@ type RegionsInfo struct { Type RegionsInfoRecommendationsSmartGroupType `json:"type"` } `json:"smartGroup"` Specific []struct { - Code string `json:"code"` + Code RegionsInfoRecommendationsSpecificCode `json:"code"` Name string `json:"name"` Provider RegionsInfoRecommendationsSpecificProvider `json:"provider"` Status *RegionsInfoRecommendationsSpecificStatus `json:"status,omitempty"` @@ -3294,6 +3445,9 @@ type RegionsInfoAllSmartGroupCode string // RegionsInfoAllSmartGroupType defines model for RegionsInfo.All.SmartGroup.Type. type RegionsInfoAllSmartGroupType string +// RegionsInfoAllSpecificCode defines model for RegionsInfo.All.Specific.Code. +type RegionsInfoAllSpecificCode string + // RegionsInfoAllSpecificProvider defines model for RegionsInfo.All.Specific.Provider. type RegionsInfoAllSpecificProvider string @@ -3309,6 +3463,9 @@ type RegionsInfoRecommendationsSmartGroupCode string // RegionsInfoRecommendationsSmartGroupType defines model for RegionsInfo.Recommendations.SmartGroup.Type. type RegionsInfoRecommendationsSmartGroupType string +// RegionsInfoRecommendationsSpecificCode defines model for RegionsInfo.Recommendations.Specific.Code. +type RegionsInfoRecommendationsSpecificCode string + // RegionsInfoRecommendationsSpecificProvider defines model for RegionsInfo.Recommendations.Specific.Provider. type RegionsInfoRecommendationsSpecificProvider string @@ -3562,6 +3719,7 @@ type UpdateApiKeyBody struct { // UpdateAuthConfigBody defines model for UpdateAuthConfigBody. type UpdateAuthConfigBody struct { ApiMaxRequestDuration nullable.Nullable[int] `json:"api_max_request_duration,omitempty"` + CustomOauthEnabled *bool `json:"custom_oauth_enabled,omitempty"` DbMaxPoolSize nullable.Nullable[int] `json:"db_max_pool_size,omitempty"` DbMaxPoolSizeUnit nullable.Nullable[UpdateAuthConfigBodyDbMaxPoolSizeUnit] `json:"db_max_pool_size_unit,omitempty"` DisableSignup nullable.Nullable[bool] `json:"disable_signup,omitempty"` @@ -3753,6 +3911,7 @@ type UpdateAuthConfigBody struct { SecurityCaptchaSecret nullable.Nullable[string] `json:"security_captcha_secret,omitempty"` SecurityManualLinkingEnabled nullable.Nullable[bool] `json:"security_manual_linking_enabled,omitempty"` SecurityRefreshTokenReuseInterval nullable.Nullable[int] `json:"security_refresh_token_reuse_interval,omitempty"` + SecuritySbForwardedForEnabled nullable.Nullable[bool] `json:"security_sb_forwarded_for_enabled,omitempty"` SecurityUpdatePasswordRequireReauthentication nullable.Nullable[bool] `json:"security_update_password_require_reauthentication,omitempty"` SessionsInactivityTimeout nullable.Nullable[int] `json:"sessions_inactivity_timeout,omitempty"` SessionsSinglePerUser nullable.Nullable[bool] `json:"sessions_single_per_user,omitempty"` @@ -3996,10 +4155,13 @@ type UpdateRealtimeConfigBody struct { // MaxPresenceEventsPerSecond Sets maximum number of presence events per second rate limit MaxPresenceEventsPerSecond *int `json:"max_presence_events_per_second,omitempty"` + // PresenceEnabled Whether to enable presence + PresenceEnabled *bool `json:"presence_enabled,omitempty"` + // PrivateOnly Whether to only allow private channels PrivateOnly *bool `json:"private_only,omitempty"` - // Suspend Whether to suspend realtime + // Suspend Disables the Realtime service for this project when true. Set to false to re-enable it. Suspend *bool `json:"suspend,omitempty"` } @@ -4144,7 +4306,9 @@ type V1CreateMigrationBody struct { // V1CreateProjectBody defines model for V1CreateProjectBody. type V1CreateProjectBody struct { // DbPass Database password - DbPass string `json:"db_pass"` + DbPass string `json:"db_pass"` + + // DesiredInstanceSize Desired instance size. Omit this field to always default to the smallest possible size. DesiredInstanceSize *V1CreateProjectBodyDesiredInstanceSize `json:"desired_instance_size,omitempty"` // KpsEnabled This field is deprecated and is ignored in this request @@ -4165,10 +4329,6 @@ type V1CreateProjectBody struct { // Deprecated: Plan *V1CreateProjectBodyPlan `json:"plan,omitempty"` - // PostgresEngine Postgres engine version. If not provided, the latest version will be used. - // Deprecated: - PostgresEngine *V1CreateProjectBodyPostgresEngine `json:"postgres_engine,omitempty"` - // Region Region you want your server to reside in. Use region_selection instead. // Deprecated: Region *V1CreateProjectBodyRegion `json:"region,omitempty"` @@ -4176,23 +4336,16 @@ type V1CreateProjectBody struct { // RegionSelection Region selection. Only one of region or region_selection can be specified. RegionSelection *V1CreateProjectBody_RegionSelection `json:"region_selection,omitempty"` - // ReleaseChannel Release channel. If not provided, GA will be used. - // Deprecated: - ReleaseChannel *V1CreateProjectBodyReleaseChannel `json:"release_channel,omitempty"` - // TemplateUrl Template URL used to create the project from the CLI. TemplateUrl *string `json:"template_url,omitempty"` } -// V1CreateProjectBodyDesiredInstanceSize defines model for V1CreateProjectBody.DesiredInstanceSize. +// V1CreateProjectBodyDesiredInstanceSize Desired instance size. Omit this field to always default to the smallest possible size. type V1CreateProjectBodyDesiredInstanceSize string // V1CreateProjectBodyPlan Subscription Plan is now set on organization level and is ignored in this request type V1CreateProjectBodyPlan string -// V1CreateProjectBodyPostgresEngine Postgres engine version. If not provided, the latest version will be used. -type V1CreateProjectBodyPostgresEngine string - // V1CreateProjectBodyRegion Region you want your server to reside in. Use region_selection instead. type V1CreateProjectBodyRegion string @@ -4227,9 +4380,6 @@ type V1CreateProjectBody_RegionSelection struct { union json.RawMessage } -// V1CreateProjectBodyReleaseChannel Release channel. If not provided, GA will be used. -type V1CreateProjectBodyReleaseChannel string - // V1GetMigrationResponse defines model for V1GetMigrationResponse. type V1GetMigrationResponse struct { CreatedBy *string `json:"created_by,omitempty"` @@ -4548,7 +4698,8 @@ type V1ServiceHealthResponseInfo1 struct { // Healthy Deprecated. Use `status` instead. // Deprecated: - Healthy bool `json:"healthy"` + Healthy bool `json:"healthy"` + ReplicationConnected bool `json:"replication_connected"` } // V1ServiceHealthResponseInfo2 defines model for . @@ -4642,6 +4793,9 @@ type V1DeleteABranchParams struct { // V1DiffABranchParams defines parameters for V1DiffABranch. type V1DiffABranchParams struct { IncludedSchemas *string `form:"included_schemas,omitempty" json:"included_schemas,omitempty"` + + // Pgdelta Use pg-delta instead of Migra for diffing when true + Pgdelta *bool `form:"pgdelta,omitempty" json:"pgdelta,omitempty"` } // V1AuthorizeUserParams defines parameters for V1AuthorizeUser. @@ -4718,7 +4872,7 @@ type V1GetAvailableRegionsParams struct { // Continent Continent code to determine regional recommendations: NA (North America), SA (South America), EU (Europe), AF (Africa), AS (Asia), OC (Oceania), AN (Antarctica) Continent *V1GetAvailableRegionsParamsContinent `form:"continent,omitempty" json:"continent,omitempty"` - // DesiredInstanceSize Desired instance size + // DesiredInstanceSize Desired instance size. Omit this field to always default to the smallest possible size. DesiredInstanceSize *V1GetAvailableRegionsParamsDesiredInstanceSize `form:"desired_instance_size,omitempty" json:"desired_instance_size,omitempty"` } @@ -4984,6 +5138,9 @@ type V1UpdatePoolerConfigJSONRequestBody = UpdateSupavisorConfigBody // V1UpdatePostgresConfigJSONRequestBody defines body for V1UpdatePostgresConfig for application/json ContentType. type V1UpdatePostgresConfigJSONRequestBody = UpdatePostgresConfigBody +// V1ModifyDatabaseDiskJSONRequestBody defines body for V1ModifyDatabaseDisk for application/json ContentType. +type V1ModifyDatabaseDiskJSONRequestBody = DiskRequestBody + // V1UpdateRealtimeConfigJSONRequestBody defines body for V1UpdateRealtimeConfig for application/json ContentType. type V1UpdateRealtimeConfigJSONRequestBody = UpdateRealtimeConfigBody @@ -5038,6 +5195,9 @@ type V1DeployAFunctionMultipartRequestBody = FunctionDeployBody // V1UpdateAFunctionJSONRequestBody defines body for V1UpdateAFunction for application/json ContentType. type V1UpdateAFunctionJSONRequestBody = V1UpdateFunctionBody +// V1UpdateJitAccessConfigJSONRequestBody defines body for V1UpdateJitAccessConfig for application/json ContentType. +type V1UpdateJitAccessConfigJSONRequestBody = JitAccessRequestRequest + // V1DeleteNetworkBansJSONRequestBody defines body for V1DeleteNetworkBans for application/json ContentType. type V1DeleteNetworkBansJSONRequestBody = RemoveNetworkBanRequest @@ -5512,6 +5672,130 @@ func (t *CreateSigningKeyBody_PrivateJwk) UnmarshalJSON(b []byte) error { return err } +// AsDiskRequestBodyAttributes0 returns the union data inside the DiskRequestBody_Attributes as a DiskRequestBodyAttributes0 +func (t DiskRequestBody_Attributes) AsDiskRequestBodyAttributes0() (DiskRequestBodyAttributes0, error) { + var body DiskRequestBodyAttributes0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromDiskRequestBodyAttributes0 overwrites any union data inside the DiskRequestBody_Attributes as the provided DiskRequestBodyAttributes0 +func (t *DiskRequestBody_Attributes) FromDiskRequestBodyAttributes0(v DiskRequestBodyAttributes0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeDiskRequestBodyAttributes0 performs a merge with any union data inside the DiskRequestBody_Attributes, using the provided DiskRequestBodyAttributes0 +func (t *DiskRequestBody_Attributes) MergeDiskRequestBodyAttributes0(v DiskRequestBodyAttributes0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsDiskRequestBodyAttributes1 returns the union data inside the DiskRequestBody_Attributes as a DiskRequestBodyAttributes1 +func (t DiskRequestBody_Attributes) AsDiskRequestBodyAttributes1() (DiskRequestBodyAttributes1, error) { + var body DiskRequestBodyAttributes1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromDiskRequestBodyAttributes1 overwrites any union data inside the DiskRequestBody_Attributes as the provided DiskRequestBodyAttributes1 +func (t *DiskRequestBody_Attributes) FromDiskRequestBodyAttributes1(v DiskRequestBodyAttributes1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeDiskRequestBodyAttributes1 performs a merge with any union data inside the DiskRequestBody_Attributes, using the provided DiskRequestBodyAttributes1 +func (t *DiskRequestBody_Attributes) MergeDiskRequestBodyAttributes1(v DiskRequestBodyAttributes1) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t DiskRequestBody_Attributes) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *DiskRequestBody_Attributes) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + +// AsDiskResponseAttributes0 returns the union data inside the DiskResponse_Attributes as a DiskResponseAttributes0 +func (t DiskResponse_Attributes) AsDiskResponseAttributes0() (DiskResponseAttributes0, error) { + var body DiskResponseAttributes0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromDiskResponseAttributes0 overwrites any union data inside the DiskResponse_Attributes as the provided DiskResponseAttributes0 +func (t *DiskResponse_Attributes) FromDiskResponseAttributes0(v DiskResponseAttributes0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeDiskResponseAttributes0 performs a merge with any union data inside the DiskResponse_Attributes, using the provided DiskResponseAttributes0 +func (t *DiskResponse_Attributes) MergeDiskResponseAttributes0(v DiskResponseAttributes0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsDiskResponseAttributes1 returns the union data inside the DiskResponse_Attributes as a DiskResponseAttributes1 +func (t DiskResponse_Attributes) AsDiskResponseAttributes1() (DiskResponseAttributes1, error) { + var body DiskResponseAttributes1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromDiskResponseAttributes1 overwrites any union data inside the DiskResponse_Attributes as the provided DiskResponseAttributes1 +func (t *DiskResponse_Attributes) FromDiskResponseAttributes1(v DiskResponseAttributes1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeDiskResponseAttributes1 performs a merge with any union data inside the DiskResponse_Attributes, using the provided DiskResponseAttributes1 +func (t *DiskResponse_Attributes) MergeDiskResponseAttributes1(v DiskResponseAttributes1) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t DiskResponse_Attributes) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *DiskResponse_Attributes) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // AsListProjectAddonsResponseAvailableAddonsVariantsId0 returns the union data inside the ListProjectAddonsResponse_AvailableAddons_Variants_Id as a ListProjectAddonsResponseAvailableAddonsVariantsId0 func (t ListProjectAddonsResponse_AvailableAddons_Variants_Id) AsListProjectAddonsResponseAvailableAddonsVariantsId0() (ListProjectAddonsResponseAvailableAddonsVariantsId0, error) { var body ListProjectAddonsResponseAvailableAddonsVariantsId0 diff --git a/pkg/config/api.go b/pkg/config/api.go index e294a38c68..3fd3b6f187 100644 --- a/pkg/config/api.go +++ b/pkg/config/api.go @@ -10,25 +10,25 @@ import ( type ( api struct { - Enabled bool `toml:"enabled"` - Schemas []string `toml:"schemas"` - ExtraSearchPath []string `toml:"extra_search_path"` - MaxRows uint `toml:"max_rows"` + Enabled bool `toml:"enabled" json:"enabled"` + Schemas []string `toml:"schemas" json:"schemas"` + ExtraSearchPath []string `toml:"extra_search_path" json:"extra_search_path"` + MaxRows uint `toml:"max_rows" json:"max_rows"` // Local only config - Image string `toml:"-"` - KongImage string `toml:"-"` - Port uint16 `toml:"port"` - Tls tlsKong `toml:"tls"` + Image string `toml:"-" json:"-"` + KongImage string `toml:"-" json:"-"` + Port uint16 `toml:"port" json:"port"` + Tls tlsKong `toml:"tls" json:"tls"` // TODO: replace [auth|studio].api_url - ExternalUrl string `toml:"external_url"` + ExternalUrl string `toml:"external_url" json:"external_url"` } tlsKong struct { - Enabled bool `toml:"enabled"` - CertPath string `toml:"cert_path"` - CertContent []byte `toml:"-"` - KeyPath string `toml:"key_path"` - KeyContent []byte `toml:"-"` + Enabled bool `toml:"enabled" json:"enabled"` + CertPath string `toml:"cert_path" json:"cert_path"` + CertContent []byte `toml:"-" json:"-"` + KeyPath string `toml:"key_path" json:"key_path"` + KeyContent []byte `toml:"-" json:"-"` } ) diff --git a/pkg/config/apikeys.go b/pkg/config/apikeys.go index 1149e5a8d1..cb1b457cab 100644 --- a/pkg/config/apikeys.go +++ b/pkg/config/apikeys.go @@ -74,7 +74,7 @@ func (a *auth) generateAPIKeys() error { func (a auth) generateJWT(role string) (string, error) { claims := CustomClaims{Issuer: "supabase-demo", Role: role} - if len(a.SigningKeysPath) > 0 { + if len(a.SigningKeysPath) > 0 && len(a.SigningKeys) > 0 { claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 365 * 10)) // 10 years return GenerateAsymmetricJWT(a.SigningKeys[0], claims) } diff --git a/pkg/config/auth.go b/pkg/config/auth.go index 3c6ee2fd47..46df702d67 100644 --- a/pkg/config/auth.go +++ b/pkg/config/auth.go @@ -86,26 +86,26 @@ func (p *Algorithm) UnmarshalText(text []byte) error { } type JWK struct { - KeyType string `json:"kty"` - KeyID string `json:"kid,omitempty"` - Use string `json:"use,omitempty"` - KeyOps []string `json:"key_ops,omitempty"` - Algorithm Algorithm `json:"alg,omitempty"` - Extractable *bool `json:"ext,omitempty"` + KeyType string `toml:"kty" json:"kty"` + KeyID string `toml:"kid,omitempty" json:"kid,omitempty"` + Use string `toml:"use,omitempty" json:"use,omitempty"` + KeyOps []string `toml:"key_ops,omitempty" json:"key_ops,omitempty"` + Algorithm Algorithm `toml:"alg,omitempty" json:"alg,omitempty"` + Extractable *bool `toml:"ext,omitempty" json:"ext,omitempty"` // RSA specific fields - Modulus string `json:"n,omitempty"` - Exponent string `json:"e,omitempty"` + Modulus string `toml:"n,omitempty" json:"n,omitempty"` + Exponent string `toml:"e,omitempty" 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"` + PrivateExponent string `toml:"d,omitempty" json:"d,omitempty"` + FirstPrimeFactor string `toml:"p,omitempty" json:"p,omitempty"` + SecondPrimeFactor string `toml:"q,omitempty" json:"q,omitempty"` + FirstFactorCRTExponent string `toml:"dp,omitempty" json:"dp,omitempty"` + SecondFactorCRTExponent string `toml:"dq,omitempty" json:"dq,omitempty"` + FirstCRTCoefficient string `toml:"qi,omitempty" json:"qi,omitempty"` // EC specific fields - Curve string `json:"crv,omitempty"` - X string `json:"x,omitempty"` - Y string `json:"y,omitempty"` + Curve string `toml:"crv,omitempty" json:"crv,omitempty"` + X string `toml:"x,omitempty" json:"x,omitempty"` + Y string `toml:"y,omitempty" json:"y,omitempty"` } // ToPublicJWK converts a JWK to a public-only version by removing private key components @@ -146,242 +146,242 @@ func (j JWK) ToPublicJWK() JWK { type ( auth struct { - Enabled bool `toml:"enabled"` - Image string `toml:"-"` - - SiteUrl string `toml:"site_url"` - AdditionalRedirectUrls []string `toml:"additional_redirect_urls"` - JwtExpiry uint `toml:"jwt_expiry"` - JwtIssuer string `toml:"jwt_issuer"` - EnableRefreshTokenRotation bool `toml:"enable_refresh_token_rotation"` - RefreshTokenReuseInterval uint `toml:"refresh_token_reuse_interval"` - EnableManualLinking bool `toml:"enable_manual_linking"` - EnableSignup bool `toml:"enable_signup"` - EnableAnonymousSignIns bool `toml:"enable_anonymous_sign_ins"` - 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"` - Hook hook `toml:"hook"` - MFA mfa `toml:"mfa"` - Sessions sessions `toml:"sessions"` - Email email `toml:"email"` - Sms sms `toml:"sms"` - External external `toml:"external"` - Web3 web3 `toml:"web3"` - OAuthServer OAuthServer `toml:"oauth_server"` + Enabled bool `toml:"enabled" json:"enabled"` + Image string `toml:"-" json:"-"` + + SiteUrl string `toml:"site_url" json:"site_url"` + AdditionalRedirectUrls []string `toml:"additional_redirect_urls" json:"additional_redirect_urls"` + JwtExpiry uint `toml:"jwt_expiry" json:"jwt_expiry"` + JwtIssuer string `toml:"jwt_issuer" json:"jwt_issuer"` + EnableRefreshTokenRotation bool `toml:"enable_refresh_token_rotation" json:"enable_refresh_token_rotation"` + RefreshTokenReuseInterval uint `toml:"refresh_token_reuse_interval" json:"refresh_token_reuse_interval"` + EnableManualLinking bool `toml:"enable_manual_linking" json:"enable_manual_linking"` + EnableSignup bool `toml:"enable_signup" json:"enable_signup"` + EnableAnonymousSignIns bool `toml:"enable_anonymous_sign_ins" json:"enable_anonymous_sign_ins"` + MinimumPasswordLength uint `toml:"minimum_password_length" json:"minimum_password_length"` + PasswordRequirements PasswordRequirements `toml:"password_requirements" json:"password_requirements"` + SigningKeysPath string `toml:"signing_keys_path" json:"signing_keys_path"` + SigningKeys []JWK `toml:"-" json:"-"` + + RateLimit rateLimit `toml:"rate_limit" json:"rate_limit"` + Captcha *captcha `toml:"captcha" json:"captcha"` + Hook hook `toml:"hook" json:"hook"` + MFA mfa `toml:"mfa" json:"mfa"` + Sessions sessions `toml:"sessions" json:"sessions"` + Email email `toml:"email" json:"email"` + Sms sms `toml:"sms" json:"sms"` + External external `toml:"external" json:"external"` + Web3 web3 `toml:"web3" json:"web3"` + OAuthServer OAuthServer `toml:"oauth_server" json:"oauth_server"` // Custom secrets can be injected from .env file - PublishableKey Secret `toml:"publishable_key"` - SecretKey Secret `toml:"secret_key"` - JwtSecret Secret `toml:"jwt_secret"` - AnonKey Secret `toml:"anon_key"` - ServiceRoleKey Secret `toml:"service_role_key"` + PublishableKey Secret `toml:"publishable_key" json:"publishable_key"` + SecretKey Secret `toml:"secret_key" json:"secret_key"` + JwtSecret Secret `toml:"jwt_secret" json:"jwt_secret"` + AnonKey Secret `toml:"anon_key" json:"anon_key"` + ServiceRoleKey Secret `toml:"service_role_key" json:"service_role_key"` - ThirdParty thirdParty `toml:"third_party"` + ThirdParty thirdParty `toml:"third_party" json:"third_party"` } external map[string]provider thirdParty struct { - Firebase tpaFirebase `toml:"firebase"` - Auth0 tpaAuth0 `toml:"auth0"` - Cognito tpaCognito `toml:"aws_cognito"` - Clerk tpaClerk `toml:"clerk"` - WorkOs tpaWorkOs `toml:"workos"` + Firebase tpaFirebase `toml:"firebase" json:"firebase"` + Auth0 tpaAuth0 `toml:"auth0" json:"auth0"` + Cognito tpaCognito `toml:"aws_cognito" json:"aws_cognito"` + Clerk tpaClerk `toml:"clerk" json:"clerk"` + WorkOs tpaWorkOs `toml:"workos" json:"workos"` } rateLimit struct { - AnonymousUsers uint `toml:"anonymous_users"` - TokenRefresh uint `toml:"token_refresh"` - SignInSignUps uint `toml:"sign_in_sign_ups"` - TokenVerifications uint `toml:"token_verifications"` - EmailSent uint `toml:"email_sent"` - SmsSent uint `toml:"sms_sent"` - Web3 uint `toml:"web3"` + AnonymousUsers uint `toml:"anonymous_users" json:"anonymous_users"` + TokenRefresh uint `toml:"token_refresh" json:"token_refresh"` + SignInSignUps uint `toml:"sign_in_sign_ups" json:"sign_in_sign_ups"` + TokenVerifications uint `toml:"token_verifications" json:"token_verifications"` + EmailSent uint `toml:"email_sent" json:"email_sent"` + SmsSent uint `toml:"sms_sent" json:"sms_sent"` + Web3 uint `toml:"web3" json:"web3"` } tpaFirebase struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" json:"enabled"` - ProjectID string `toml:"project_id"` + ProjectID string `toml:"project_id" json:"project_id"` } tpaAuth0 struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" json:"enabled"` - Tenant string `toml:"tenant"` - TenantRegion string `toml:"tenant_region"` + Tenant string `toml:"tenant" json:"tenant"` + TenantRegion string `toml:"tenant_region" json:"tenant_region"` } tpaCognito struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" json:"enabled"` - UserPoolID string `toml:"user_pool_id"` - UserPoolRegion string `toml:"user_pool_region"` + UserPoolID string `toml:"user_pool_id" json:"user_pool_id"` + UserPoolRegion string `toml:"user_pool_region" json:"user_pool_region"` } tpaClerk struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" json:"enabled"` - Domain string `toml:"domain"` + Domain string `toml:"domain" json:"domain"` } tpaWorkOs struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" json:"enabled"` - IssuerUrl string `toml:"issuer_url"` + IssuerUrl string `toml:"issuer_url" json:"issuer_url"` } email struct { - EnableSignup bool `toml:"enable_signup"` - DoubleConfirmChanges bool `toml:"double_confirm_changes"` - EnableConfirmations bool `toml:"enable_confirmations"` - SecurePasswordChange bool `toml:"secure_password_change"` - Template map[string]emailTemplate `toml:"template"` - Notification map[string]notification `toml:"notification"` - Smtp *smtp `toml:"smtp"` - MaxFrequency time.Duration `toml:"max_frequency"` - OtpLength uint `toml:"otp_length"` - OtpExpiry uint `toml:"otp_expiry"` + EnableSignup bool `toml:"enable_signup" json:"enable_signup"` + DoubleConfirmChanges bool `toml:"double_confirm_changes" json:"double_confirm_changes"` + EnableConfirmations bool `toml:"enable_confirmations" json:"enable_confirmations"` + SecurePasswordChange bool `toml:"secure_password_change" json:"secure_password_change"` + Template map[string]emailTemplate `toml:"template" json:"template"` + Notification map[string]notification `toml:"notification" json:"notification"` + Smtp *smtp `toml:"smtp" json:"smtp"` + MaxFrequency time.Duration `toml:"max_frequency" json:"max_frequency"` + OtpLength uint `toml:"otp_length" json:"otp_length"` + OtpExpiry uint `toml:"otp_expiry" json:"otp_expiry"` } smtp struct { - Enabled bool `toml:"enabled"` - Host string `toml:"host"` - Port uint16 `toml:"port"` - User string `toml:"user"` - Pass Secret `toml:"pass"` - AdminEmail openapi_types.Email `toml:"admin_email"` - SenderName string `toml:"sender_name"` + Enabled bool `toml:"enabled" json:"enabled"` + Host string `toml:"host" json:"host"` + Port uint16 `toml:"port" json:"port"` + User string `toml:"user" json:"user"` + Pass Secret `toml:"pass" json:"pass"` + AdminEmail openapi_types.Email `toml:"admin_email" json:"admin_email"` + SenderName string `toml:"sender_name" json:"sender_name"` } emailTemplate struct { - Subject *string `toml:"subject"` - Content *string `toml:"content"` + Subject *string `toml:"subject" json:"subject"` + Content *string `toml:"content" json:"content"` // Only content path is accepted in config.toml - ContentPath string `toml:"content_path"` + ContentPath string `toml:"content_path" json:"content_path"` } notification struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" json:"enabled"` emailTemplate } sms struct { - EnableSignup bool `toml:"enable_signup"` - EnableConfirmations bool `toml:"enable_confirmations"` - Template string `toml:"template"` - Twilio twilioConfig `toml:"twilio"` - TwilioVerify twilioConfig `toml:"twilio_verify"` - Messagebird messagebirdConfig `toml:"messagebird"` - Textlocal textlocalConfig `toml:"textlocal"` - Vonage vonageConfig `toml:"vonage"` - TestOTP map[string]string `toml:"test_otp"` - MaxFrequency time.Duration `toml:"max_frequency"` + EnableSignup bool `toml:"enable_signup" json:"enable_signup"` + EnableConfirmations bool `toml:"enable_confirmations" json:"enable_confirmations"` + Template string `toml:"template" json:"template"` + Twilio twilioConfig `toml:"twilio" json:"twilio"` + TwilioVerify twilioConfig `toml:"twilio_verify" json:"twilio_verify"` + Messagebird messagebirdConfig `toml:"messagebird" json:"messagebird"` + Textlocal textlocalConfig `toml:"textlocal" json:"textlocal"` + Vonage vonageConfig `toml:"vonage" json:"vonage"` + TestOTP map[string]string `toml:"test_otp" json:"test_otp"` + MaxFrequency time.Duration `toml:"max_frequency" json:"max_frequency"` } captcha struct { - Enabled bool `toml:"enabled"` - Provider CaptchaProvider `toml:"provider"` - Secret Secret `toml:"secret"` + Enabled bool `toml:"enabled" json:"enabled"` + Provider CaptchaProvider `toml:"provider" json:"provider"` + Secret Secret `toml:"secret" json:"secret"` } hook struct { - MFAVerificationAttempt *hookConfig `toml:"mfa_verification_attempt"` - PasswordVerificationAttempt *hookConfig `toml:"password_verification_attempt"` - CustomAccessToken *hookConfig `toml:"custom_access_token"` - SendSMS *hookConfig `toml:"send_sms"` - SendEmail *hookConfig `toml:"send_email"` - BeforeUserCreated *hookConfig `toml:"before_user_created"` + MFAVerificationAttempt *hookConfig `toml:"mfa_verification_attempt" json:"mfa_verification_attempt"` + PasswordVerificationAttempt *hookConfig `toml:"password_verification_attempt" json:"password_verification_attempt"` + CustomAccessToken *hookConfig `toml:"custom_access_token" json:"custom_access_token"` + SendSMS *hookConfig `toml:"send_sms" json:"send_sms"` + SendEmail *hookConfig `toml:"send_email" json:"send_email"` + BeforeUserCreated *hookConfig `toml:"before_user_created" json:"before_user_created"` } factorTypeConfiguration struct { - EnrollEnabled bool `toml:"enroll_enabled"` - VerifyEnabled bool `toml:"verify_enabled"` + EnrollEnabled bool `toml:"enroll_enabled" json:"enroll_enabled"` + VerifyEnabled bool `toml:"verify_enabled" json:"verify_enabled"` } phoneFactorTypeConfiguration struct { factorTypeConfiguration - OtpLength uint `toml:"otp_length"` - Template string `toml:"template"` - MaxFrequency time.Duration `toml:"max_frequency"` + OtpLength uint `toml:"otp_length" json:"otp_length"` + Template string `toml:"template" json:"template"` + MaxFrequency time.Duration `toml:"max_frequency" json:"max_frequency"` } mfa struct { - TOTP factorTypeConfiguration `toml:"totp"` - Phone phoneFactorTypeConfiguration `toml:"phone"` - WebAuthn factorTypeConfiguration `toml:"web_authn"` - MaxEnrolledFactors uint `toml:"max_enrolled_factors"` + TOTP factorTypeConfiguration `toml:"totp" json:"totp"` + Phone phoneFactorTypeConfiguration `toml:"phone" json:"phone"` + WebAuthn factorTypeConfiguration `toml:"web_authn" json:"web_authn"` + MaxEnrolledFactors uint `toml:"max_enrolled_factors" json:"max_enrolled_factors"` } hookConfig struct { - Enabled bool `toml:"enabled"` - URI string `toml:"uri"` - Secrets Secret `toml:"secrets"` + Enabled bool `toml:"enabled" json:"enabled"` + URI string `toml:"uri" json:"uri"` + Secrets Secret `toml:"secrets" json:"secrets"` } sessions struct { - Timebox time.Duration `toml:"timebox"` - InactivityTimeout time.Duration `toml:"inactivity_timeout"` + Timebox time.Duration `toml:"timebox" json:"timebox"` + InactivityTimeout time.Duration `toml:"inactivity_timeout" json:"inactivity_timeout"` } twilioConfig struct { - Enabled bool `toml:"enabled"` - AccountSid string `toml:"account_sid"` - MessageServiceSid string `toml:"message_service_sid"` - AuthToken Secret `toml:"auth_token"` + Enabled bool `toml:"enabled" json:"enabled"` + AccountSid string `toml:"account_sid" json:"account_sid"` + MessageServiceSid string `toml:"message_service_sid" json:"message_service_sid"` + AuthToken Secret `toml:"auth_token" json:"auth_token"` } messagebirdConfig struct { - Enabled bool `toml:"enabled"` - Originator string `toml:"originator"` - AccessKey Secret `toml:"access_key"` + Enabled bool `toml:"enabled" json:"enabled"` + Originator string `toml:"originator" json:"originator"` + AccessKey Secret `toml:"access_key" json:"access_key"` } textlocalConfig struct { - Enabled bool `toml:"enabled"` - Sender string `toml:"sender"` - ApiKey Secret `toml:"api_key"` + Enabled bool `toml:"enabled" json:"enabled"` + Sender string `toml:"sender" json:"sender"` + ApiKey Secret `toml:"api_key" json:"api_key"` } vonageConfig struct { - Enabled bool `toml:"enabled"` - From string `toml:"from"` - ApiKey string `toml:"api_key"` - ApiSecret Secret `toml:"api_secret"` + Enabled bool `toml:"enabled" json:"enabled"` + From string `toml:"from" json:"from"` + ApiKey string `toml:"api_key" json:"api_key"` + ApiSecret Secret `toml:"api_secret" json:"api_secret"` } provider struct { - Enabled bool `toml:"enabled"` - ClientId string `toml:"client_id"` - Secret Secret `toml:"secret"` - Url string `toml:"url"` - RedirectUri string `toml:"redirect_uri"` - SkipNonceCheck bool `toml:"skip_nonce_check"` - EmailOptional bool `toml:"email_optional"` + Enabled bool `toml:"enabled" json:"enabled"` + ClientId string `toml:"client_id" json:"client_id"` + Secret Secret `toml:"secret" json:"secret"` + Url string `toml:"url" json:"url"` + RedirectUri string `toml:"redirect_uri" json:"redirect_uri"` + SkipNonceCheck bool `toml:"skip_nonce_check" json:"skip_nonce_check"` + EmailOptional bool `toml:"email_optional" json:"email_optional"` } solana struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" json:"enabled"` } ethereum struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" json:"enabled"` } web3 struct { - Solana solana `toml:"solana"` - Ethereum ethereum `toml:"ethereum"` + Solana solana `toml:"solana" json:"solana"` + Ethereum ethereum `toml:"ethereum" json:"ethereum"` } OAuthServer struct { - Enabled bool `toml:"enabled"` - AllowDynamicRegistration bool `toml:"allow_dynamic_registration"` - AuthorizationUrlPath string `toml:"authorization_url_path"` + Enabled bool `toml:"enabled" json:"enabled"` + AllowDynamicRegistration bool `toml:"allow_dynamic_registration" json:"allow_dynamic_registration"` + AuthorizationUrlPath string `toml:"authorization_url_path" json:"authorization_url_path"` } ) diff --git a/pkg/config/config.go b/pkg/config/config.go index 8cf2e4a278..a49e84d7df 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -138,112 +138,112 @@ func (g Glob) Files(fsys fs.FS) ([]string, error) { type ( // Common config fields between our "base" config and any "remote" branch specific baseConfig struct { - ProjectId string `toml:"project_id"` - Hostname string `toml:"-"` - Api api `toml:"api"` - Db db `toml:"db"` - Realtime realtime `toml:"realtime"` - Studio studio `toml:"studio"` - Inbucket inbucket `toml:"inbucket"` - Storage storage `toml:"storage"` - Auth auth `toml:"auth"` - EdgeRuntime edgeRuntime `toml:"edge_runtime"` - Functions FunctionConfig `toml:"functions"` - Analytics analytics `toml:"analytics"` - Experimental experimental `toml:"experimental"` + ProjectId string `toml:"project_id" json:"project_id"` + Hostname string `toml:"-" json:"-"` + Api api `toml:"api" json:"api"` + Db db `toml:"db" json:"db"` + Realtime realtime `toml:"realtime" json:"realtime"` + Studio studio `toml:"studio" json:"studio"` + Inbucket inbucket `toml:"inbucket" json:"inbucket"` + Storage storage `toml:"storage" json:"storage"` + Auth auth `toml:"auth" json:"auth"` + EdgeRuntime edgeRuntime `toml:"edge_runtime" json:"edge_runtime"` + Functions FunctionConfig `toml:"functions" json:"functions"` + Analytics analytics `toml:"analytics" json:"analytics"` + Experimental experimental `toml:"experimental" json:"experimental"` } config struct { baseConfig - Remotes map[string]baseConfig `toml:"remotes"` + Remotes map[string]baseConfig `toml:"remotes" json:"remotes"` } realtime struct { - Enabled bool `toml:"enabled"` - Image string `toml:"-"` - IpVersion AddressFamily `toml:"ip_version"` - MaxHeaderLength uint `toml:"max_header_length"` - TenantId string `toml:"-"` - EncryptionKey string `toml:"-"` - SecretKeyBase string `toml:"-"` + Enabled bool `toml:"enabled" json:"enabled"` + Image string `toml:"-" json:"-"` + IpVersion AddressFamily `toml:"ip_version" json:"ip_version"` + MaxHeaderLength uint `toml:"max_header_length" json:"max_header_length"` + TenantId string `toml:"-" json:"-"` + EncryptionKey string `toml:"-" json:"-"` + SecretKeyBase string `toml:"-" json:"-"` } studio struct { - Enabled bool `toml:"enabled"` - Image string `toml:"-"` - Port uint16 `toml:"port"` - ApiUrl string `toml:"api_url"` - OpenaiApiKey Secret `toml:"openai_api_key"` - PgmetaImage string `toml:"-"` + Enabled bool `toml:"enabled" json:"enabled"` + Image string `toml:"-" json:"-"` + Port uint16 `toml:"port" json:"port"` + ApiUrl string `toml:"api_url" json:"api_url"` + OpenaiApiKey Secret `toml:"openai_api_key" json:"openai_api_key"` + PgmetaImage string `toml:"-" json:"-"` } inbucket struct { - Enabled bool `toml:"enabled"` - Image string `toml:"-"` - Port uint16 `toml:"port"` - SmtpPort uint16 `toml:"smtp_port"` - Pop3Port uint16 `toml:"pop3_port"` - AdminEmail string `toml:"admin_email"` - SenderName string `toml:"sender_name"` + Enabled bool `toml:"enabled" json:"enabled"` + Image string `toml:"-" json:"-"` + Port uint16 `toml:"port" json:"port"` + SmtpPort uint16 `toml:"smtp_port" json:"smtp_port"` + Pop3Port uint16 `toml:"pop3_port" json:"pop3_port"` + AdminEmail string `toml:"admin_email" json:"admin_email"` + SenderName string `toml:"sender_name" json:"sender_name"` } edgeRuntime struct { - Enabled bool `toml:"enabled"` - Image string `toml:"-"` - Policy RequestPolicy `toml:"policy"` - InspectorPort uint16 `toml:"inspector_port"` - Secrets SecretsConfig `toml:"secrets"` - DenoVersion uint `toml:"deno_version"` + Enabled bool `toml:"enabled" json:"enabled"` + Image string `toml:"-" json:"-"` + Policy RequestPolicy `toml:"policy" json:"policy"` + InspectorPort uint16 `toml:"inspector_port" json:"inspector_port"` + Secrets SecretsConfig `toml:"secrets" json:"secrets"` + DenoVersion uint `toml:"deno_version" json:"deno_version"` } SecretsConfig map[string]Secret FunctionConfig map[string]function function struct { - Enabled bool `toml:"enabled" json:"-"` - VerifyJWT bool `toml:"verify_jwt" json:"verifyJWT"` - ImportMap string `toml:"import_map" json:"importMapPath,omitempty"` - Entrypoint string `toml:"entrypoint" json:"entrypointPath,omitempty"` - StaticFiles Glob `toml:"static_files" json:"staticFiles,omitempty"` + Enabled bool `toml:"enabled" json:"enabled"` + VerifyJWT bool `toml:"verify_jwt" json:"verify_jwt"` + ImportMap string `toml:"import_map" json:"import_map"` + Entrypoint string `toml:"entrypoint" json:"entrypoint"` + StaticFiles Glob `toml:"static_files" json:"static_files"` } analytics struct { - Enabled bool `toml:"enabled"` - Image string `toml:"-"` - VectorImage string `toml:"-"` - Port uint16 `toml:"port"` - Backend LogflareBackend `toml:"backend"` - GcpProjectId string `toml:"gcp_project_id"` - GcpProjectNumber string `toml:"gcp_project_number"` - GcpJwtPath string `toml:"gcp_jwt_path"` - ApiKey string `toml:"-"` + Enabled bool `toml:"enabled" json:"enabled"` + Image string `toml:"-" json:"-"` + VectorImage string `toml:"-" json:"-"` + Port uint16 `toml:"port" json:"port"` + Backend LogflareBackend `toml:"backend" json:"backend"` + GcpProjectId string `toml:"gcp_project_id" json:"gcp_project_id"` + GcpProjectNumber string `toml:"gcp_project_number" json:"gcp_project_number"` + GcpJwtPath string `toml:"gcp_jwt_path" json:"gcp_jwt_path"` + ApiKey string `toml:"-" json:"-"` // Deprecated together with syslog - VectorPort uint16 `toml:"vector_port"` + VectorPort uint16 `toml:"vector_port" json:"vector_port"` } webhooks struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" json:"enabled"` } inspect struct { - Rules []rule `toml:"rules"` + Rules []rule `toml:"rules" json:"rules"` } rule struct { - Query string `toml:"query"` - Name string `toml:"name"` - Pass string `toml:"pass"` - Fail string `toml:"fail"` + Query string `toml:"query" json:"query"` + Name string `toml:"name" json:"name"` + Pass string `toml:"pass" json:"pass"` + Fail string `toml:"fail" json:"fail"` } experimental struct { - OrioleDBVersion string `toml:"orioledb_version"` - S3Host string `toml:"s3_host"` - S3Region string `toml:"s3_region"` - S3AccessKey string `toml:"s3_access_key"` - S3SecretKey string `toml:"s3_secret_key"` - Webhooks *webhooks `toml:"webhooks"` - Inspect inspect `toml:"inspect"` + OrioleDBVersion string `toml:"orioledb_version" json:"orioledb_version"` + S3Host string `toml:"s3_host" json:"s3_host"` + S3Region string `toml:"s3_region" json:"s3_region"` + S3AccessKey string `toml:"s3_access_key" json:"s3_access_key"` + S3SecretKey string `toml:"s3_secret_key" json:"s3_secret_key"` + Webhooks *webhooks `toml:"webhooks" json:"webhooks"` + Inspect inspect `toml:"inspect" json:"inspect"` } ) @@ -551,7 +551,7 @@ func (c *config) load(v *viper.Viper) error { } } if err := v.UnmarshalExact(c, func(dc *mapstructure.DecoderConfig) { - dc.TagName = "toml" + dc.TagName = "json" dc.Squash = true dc.ZeroFields = true dc.DecodeHook = c.newDecodeHook(LoadEnvHook, ValidateFunctionsHook) diff --git a/pkg/config/db.go b/pkg/config/db.go index 122f5364bc..7b0c76a163 100644 --- a/pkg/config/db.go +++ b/pkg/config/db.go @@ -44,74 +44,79 @@ func (r *SessionReplicationRole) UnmarshalText(text []byte) error { type ( settings struct { - EffectiveCacheSize *string `toml:"effective_cache_size"` - LogicalDecodingWorkMem *string `toml:"logical_decoding_work_mem"` - MaintenanceWorkMem *string `toml:"maintenance_work_mem"` - MaxConnections *uint `toml:"max_connections"` - MaxLocksPerTransaction *uint `toml:"max_locks_per_transaction"` - MaxParallelMaintenanceWorkers *uint `toml:"max_parallel_maintenance_workers"` - MaxParallelWorkers *uint `toml:"max_parallel_workers"` - MaxParallelWorkersPerGather *uint `toml:"max_parallel_workers_per_gather"` - MaxReplicationSlots *uint `toml:"max_replication_slots"` - MaxSlotWalKeepSize *string `toml:"max_slot_wal_keep_size"` - MaxStandbyArchiveDelay *string `toml:"max_standby_archive_delay"` - MaxStandbyStreamingDelay *string `toml:"max_standby_streaming_delay"` - MaxWalSize *string `toml:"max_wal_size"` - MaxWalSenders *uint `toml:"max_wal_senders"` - MaxWorkerProcesses *uint `toml:"max_worker_processes"` - SessionReplicationRole *SessionReplicationRole `toml:"session_replication_role"` - SharedBuffers *string `toml:"shared_buffers"` - StatementTimeout *string `toml:"statement_timeout"` - TrackActivityQuerySize *string `toml:"track_activity_query_size"` - TrackCommitTimestamp *bool `toml:"track_commit_timestamp"` - WalKeepSize *string `toml:"wal_keep_size"` - WalSenderTimeout *string `toml:"wal_sender_timeout"` - WorkMem *string `toml:"work_mem"` + EffectiveCacheSize *string `toml:"effective_cache_size" json:"effective_cache_size"` + LogicalDecodingWorkMem *string `toml:"logical_decoding_work_mem" json:"logical_decoding_work_mem"` + MaintenanceWorkMem *string `toml:"maintenance_work_mem" json:"maintenance_work_mem"` + MaxConnections *uint `toml:"max_connections" json:"max_connections"` + MaxLocksPerTransaction *uint `toml:"max_locks_per_transaction" json:"max_locks_per_transaction"` + MaxParallelMaintenanceWorkers *uint `toml:"max_parallel_maintenance_workers" json:"max_parallel_maintenance_workers"` + MaxParallelWorkers *uint `toml:"max_parallel_workers" json:"max_parallel_workers"` + MaxParallelWorkersPerGather *uint `toml:"max_parallel_workers_per_gather" json:"max_parallel_workers_per_gather"` + MaxReplicationSlots *uint `toml:"max_replication_slots" json:"max_replication_slots"` + MaxSlotWalKeepSize *string `toml:"max_slot_wal_keep_size" json:"max_slot_wal_keep_size"` + MaxStandbyArchiveDelay *string `toml:"max_standby_archive_delay" json:"max_standby_archive_delay"` + MaxStandbyStreamingDelay *string `toml:"max_standby_streaming_delay" json:"max_standby_streaming_delay"` + MaxWalSize *string `toml:"max_wal_size" json:"max_wal_size"` + MaxWalSenders *uint `toml:"max_wal_senders" json:"max_wal_senders"` + MaxWorkerProcesses *uint `toml:"max_worker_processes" json:"max_worker_processes"` + SessionReplicationRole *SessionReplicationRole `toml:"session_replication_role" json:"session_replication_role"` + SharedBuffers *string `toml:"shared_buffers" json:"shared_buffers"` + StatementTimeout *string `toml:"statement_timeout" json:"statement_timeout"` + TrackActivityQuerySize *string `toml:"track_activity_query_size" json:"track_activity_query_size"` + TrackCommitTimestamp *bool `toml:"track_commit_timestamp" json:"track_commit_timestamp"` + WalKeepSize *string `toml:"wal_keep_size" json:"wal_keep_size"` + WalSenderTimeout *string `toml:"wal_sender_timeout" json:"wal_sender_timeout"` + WorkMem *string `toml:"work_mem" json:"work_mem"` } networkRestrictions struct { - Enabled bool `toml:"enabled"` - AllowedCidrs []string `toml:"allowed_cidrs"` - AllowedCidrsV6 []string `toml:"allowed_cidrs_v6"` + Enabled bool `toml:"enabled" json:"enabled"` + AllowedCidrs []string `toml:"allowed_cidrs" json:"allowed_cidrs"` + AllowedCidrsV6 []string `toml:"allowed_cidrs_v6" json:"allowed_cidrs_v6"` + } + + sslEnforcement struct { + Enabled bool `toml:"enabled" json:"enabled"` } db struct { - Image string `toml:"-"` - Port uint16 `toml:"port"` - ShadowPort uint16 `toml:"shadow_port"` - HealthTimeout time.Duration `toml:"health_timeout"` - MajorVersion uint `toml:"major_version"` - Password string `toml:"-"` - RootKey Secret `toml:"root_key"` - Pooler pooler `toml:"pooler"` - Migrations migrations `toml:"migrations"` - Seed seed `toml:"seed"` - Settings settings `toml:"settings"` - NetworkRestrictions networkRestrictions `toml:"network_restrictions"` - Vault map[string]Secret `toml:"vault"` + Image string `toml:"-" json:"-"` + Port uint16 `toml:"port" json:"port"` + ShadowPort uint16 `toml:"shadow_port" json:"shadow_port"` + HealthTimeout time.Duration `toml:"health_timeout" json:"health_timeout"` + MajorVersion uint `toml:"major_version" json:"major_version"` + Password string `toml:"-" json:"-"` + RootKey Secret `toml:"root_key" json:"root_key"` + Pooler pooler `toml:"pooler" json:"pooler"` + Migrations migrations `toml:"migrations" json:"migrations"` + Seed seed `toml:"seed" json:"seed"` + Settings settings `toml:"settings" json:"settings"` + NetworkRestrictions networkRestrictions `toml:"network_restrictions" json:"network_restrictions"` + SslEnforcement *sslEnforcement `toml:"ssl_enforcement" json:"ssl_enforcement"` + Vault map[string]Secret `toml:"vault" json:"vault"` } migrations struct { - Enabled bool `toml:"enabled"` - SchemaPaths Glob `toml:"schema_paths"` + Enabled bool `toml:"enabled" json:"enabled"` + SchemaPaths Glob `toml:"schema_paths" json:"schema_paths"` } seed struct { - Enabled bool `toml:"enabled"` - SqlPaths Glob `toml:"sql_paths"` + Enabled bool `toml:"enabled" json:"enabled"` + SqlPaths Glob `toml:"sql_paths" json:"sql_paths"` } pooler struct { - Enabled bool `toml:"enabled"` - Image string `toml:"-"` - Port uint16 `toml:"port"` - PoolMode PoolMode `toml:"pool_mode"` - DefaultPoolSize uint `toml:"default_pool_size"` - MaxClientConn uint `toml:"max_client_conn"` - ConnectionString string `toml:"-"` - TenantId string `toml:"-"` - EncryptionKey string `toml:"-"` - SecretKeyBase string `toml:"-"` + Enabled bool `toml:"enabled" json:"enabled"` + Image string `toml:"-" json:"-"` + Port uint16 `toml:"port" json:"port"` + PoolMode PoolMode `toml:"pool_mode" json:"pool_mode"` + DefaultPoolSize uint `toml:"default_pool_size" json:"default_pool_size"` + MaxClientConn uint `toml:"max_client_conn" json:"max_client_conn"` + ConnectionString string `toml:"-" json:"-"` + TenantId string `toml:"-" json:"-"` + EncryptionKey string `toml:"-" json:"-"` + SecretKeyBase string `toml:"-" json:"-"` } ) @@ -233,3 +238,31 @@ func (n *networkRestrictions) DiffWithRemote(remoteConfig v1API.NetworkRestricti } return diff.Diff("remote[db.network_restrictions]", remoteCompare, "local[db.network_restrictions]", currentValue), nil } + +func (s sslEnforcement) ToUpdateSslEnforcementBody() v1API.V1UpdateSslEnforcementConfigJSONRequestBody { + body := v1API.V1UpdateSslEnforcementConfigJSONRequestBody{} + body.RequestedConfig.Database = s.Enabled + return body +} + +func (s *sslEnforcement) FromRemoteSslEnforcement(remoteConfig v1API.SslEnforcementResponse) { + if s == nil { + return + } + s.Enabled = remoteConfig.CurrentConfig.Database +} + +func (s *sslEnforcement) DiffWithRemote(remoteConfig v1API.SslEnforcementResponse) ([]byte, error) { + copy := *s + // Convert the config values into easily comparable remoteConfig values + currentValue, err := ToTomlBytes(copy) + if err != nil { + return nil, err + } + copy.FromRemoteSslEnforcement(remoteConfig) + remoteCompare, err := ToTomlBytes(copy) + if err != nil { + return nil, err + } + return diff.Diff("remote[db.ssl_enforcement]", remoteCompare, "local[db.ssl_enforcement]", currentValue), nil +} diff --git a/pkg/config/storage.go b/pkg/config/storage.go index 7fdffb3a85..740c525f43 100644 --- a/pkg/config/storage.go +++ b/pkg/config/storage.go @@ -8,55 +8,55 @@ import ( type ( storage struct { - Enabled bool `toml:"enabled"` - Image string `toml:"-"` - TargetMigration string `toml:"-"` - ImgProxyImage string `toml:"-"` - FileSizeLimit sizeInBytes `toml:"file_size_limit"` - ImageTransformation *imageTransformation `toml:"image_transformation"` - S3Protocol *s3Protocol `toml:"s3_protocol"` - S3Credentials storageS3Credentials `toml:"-"` - Buckets BucketConfig `toml:"buckets"` - AnalyticsBuckets analyticsBuckets `toml:"analytics"` - VectorBuckets vectorBuckets `toml:"vector"` + Enabled bool `toml:"enabled" json:"enabled"` + Image string `toml:"-" json:"-"` + TargetMigration string `toml:"-" json:"-"` + ImgProxyImage string `toml:"-" json:"-"` + FileSizeLimit sizeInBytes `toml:"file_size_limit" json:"file_size_limit"` + ImageTransformation *imageTransformation `toml:"image_transformation" json:"image_transformation"` + S3Protocol *s3Protocol `toml:"s3_protocol" json:"s3_protocol"` + S3Credentials storageS3Credentials `toml:"-" json:"-"` + Buckets BucketConfig `toml:"buckets" json:"buckets"` + AnalyticsBuckets analyticsBuckets `toml:"analytics" json:"analytics"` + VectorBuckets vectorBuckets `toml:"vector" json:"vector"` } imageTransformation struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" json:"enabled"` } analyticsBuckets struct { - Enabled bool `toml:"enabled"` - MaxNamespaces uint `toml:"max_namespaces"` - MaxTables uint `toml:"max_tables"` - MaxCatalogs uint `toml:"max_catalogs"` - Buckets map[string]struct{} `toml:"buckets"` + Enabled bool `toml:"enabled" json:"enabled"` + MaxNamespaces uint `toml:"max_namespaces" json:"max_namespaces"` + MaxTables uint `toml:"max_tables" json:"max_tables"` + MaxCatalogs uint `toml:"max_catalogs" json:"max_catalogs"` + Buckets map[string]struct{} `toml:"buckets" json:"buckets"` } vectorBuckets struct { - Enabled bool `toml:"enabled"` - MaxBuckets uint `toml:"max_buckets"` - MaxIndexes uint `toml:"max_indexes"` - Buckets map[string]struct{} `toml:"buckets"` + Enabled bool `toml:"enabled" json:"enabled"` + MaxBuckets uint `toml:"max_buckets" json:"max_buckets"` + MaxIndexes uint `toml:"max_indexes" json:"max_indexes"` + Buckets map[string]struct{} `toml:"buckets" json:"buckets"` } s3Protocol struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" json:"enabled"` } storageS3Credentials struct { - AccessKeyId string `toml:"-"` - SecretAccessKey string `toml:"-"` - Region string `toml:"-"` + AccessKeyId string `toml:"-" json:"-"` + SecretAccessKey string `toml:"-" json:"-"` + Region string `toml:"-" json:"-"` } BucketConfig map[string]bucket bucket struct { - Public *bool `toml:"public"` - FileSizeLimit sizeInBytes `toml:"file_size_limit"` - AllowedMimeTypes []string `toml:"allowed_mime_types"` - ObjectsPath string `toml:"objects_path"` + Public *bool `toml:"public" json:"public"` + FileSizeLimit sizeInBytes `toml:"file_size_limit" json:"file_size_limit"` + AllowedMimeTypes []string `toml:"allowed_mime_types" json:"allowed_mime_types"` + ObjectsPath string `toml:"objects_path" json:"objects_path"` } ) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index bb587fe420..6c21addec8 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.6.1.075 AS pg +FROM supabase/postgres:17.6.1.095 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:v14.3 AS postgrest -FROM supabase/postgres-meta:v0.95.2 AS pgmeta -FROM supabase/studio:2026.01.27-sha-2a37755 AS studio +FROM postgrest/postgrest:v14.5 AS postgrest +FROM supabase/postgres-meta:v0.96.1 AS pgmeta +FROM supabase/studio:2026.03.04-sha-0043607 AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy -FROM supabase/edge-runtime:v1.70.0 AS edgeruntime +FROM supabase/edge-runtime:v1.71.0 AS edgeruntime FROM timberio/vector:0.28.1-alpine AS vector FROM supabase/supavisor:2.7.4 AS supavisor -FROM supabase/gotrue:v2.186.0 AS gotrue -FROM supabase/realtime:v2.73.2 AS realtime -FROM supabase/storage-api:v1.35.3 AS storage -FROM supabase/logflare:1.30.5 AS logflare +FROM supabase/gotrue:v2.187.0 AS gotrue +FROM supabase/realtime:v2.78.10 AS realtime +FROM supabase/storage-api:v1.41.8 AS storage +FROM supabase/logflare:1.34.7 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 87af2c3899..44b58cdb0c 100644 --- a/pkg/config/templates/config.toml +++ b/pkg/config/templates/config.toml @@ -74,6 +74,10 @@ allowed_cidrs = ["0.0.0.0/0"] # Defaults to allow all IPv6 connections. Set empty array to block all IPs. allowed_cidrs_v6 = ["::/0"] +# Uncomment to reject non-secure connections to the database. +# [db.ssl_enforcement] +# enabled = true + [realtime] enabled = true # Bind realtime via either IPv4 or IPv6. (default: IPv4) diff --git a/pkg/config/testdata/config.toml b/pkg/config/testdata/config.toml index ad2a0dd3e6..b228a9c073 100644 --- a/pkg/config/testdata/config.toml +++ b/pkg/config/testdata/config.toml @@ -74,6 +74,10 @@ allowed_cidrs = ["0.0.0.0/0"] # Defaults to allow all IPv6 connections. Set empty array to block all IPs. allowed_cidrs_v6 = ["::/0"] +# Uncomment to reject non-secure connections to the database. +[db.ssl_enforcement] +enabled = true + [realtime] enabled = true # Bind realtime via either IPv4 or IPv6. (default: IPv6) diff --git a/pkg/config/updater.go b/pkg/config/updater.go index 8915f43fb3..6c7e7eecd1 100644 --- a/pkg/config/updater.go +++ b/pkg/config/updater.go @@ -100,10 +100,16 @@ func (u *ConfigUpdater) UpdateDbConfig(ctx context.Context, projectRef string, c if err := u.UpdateDbNetworkRestrictionsConfig(ctx, projectRef, c.NetworkRestrictions, filter...); err != nil { return err } + if c.SslEnforcement != nil { + return u.UpdateSslEnforcement(ctx, projectRef, *c.SslEnforcement, filter...) + } return nil } func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, projectRef string, n networkRestrictions, filter ...func(string) bool) error { + if !n.Enabled { + return nil + } networkRestrictionsConfig, err := u.client.V1GetNetworkRestrictionsWithResponse(ctx, projectRef) if err != nil { return errors.Errorf("failed to read network restrictions config: %w", err) @@ -132,6 +138,35 @@ func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, p return nil } +func (u *ConfigUpdater) UpdateSslEnforcement(ctx context.Context, projectRef string, s sslEnforcement, filter ...func(string) bool) error { + sslEnforcementConfig, err := u.client.V1GetSslEnforcementConfigWithResponse(ctx, projectRef) + if err != nil { + return errors.Errorf("failed to read SSL enforcement config: %w", err) + } else if sslEnforcementConfig.JSON200 == nil { + return errors.Errorf("unexpected status %d: %s", sslEnforcementConfig.StatusCode(), string(sslEnforcementConfig.Body)) + } + sslEnforcementDiff, err := s.DiffWithRemote(*sslEnforcementConfig.JSON200) + if err != nil { + return err + } else if len(sslEnforcementDiff) == 0 { + fmt.Fprintln(os.Stderr, "Remote DB SSL enforcement config is up to date.") + return nil + } + fmt.Fprintln(os.Stderr, "Updating SSL enforcement with config:", string(sslEnforcementDiff)) + for _, keep := range filter { + if !keep("db") { + return nil + } + } + updateBody := s.ToUpdateSslEnforcementBody() + if resp, err := u.client.V1UpdateSslEnforcementConfigWithResponse(ctx, projectRef, updateBody); err != nil { + return errors.Errorf("failed to update SSL enforcement config: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body)) + } + return nil +} + func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, c auth, filter ...func(string) bool) error { if !c.Enabled { return nil diff --git a/pkg/config/updater_test.go b/pkg/config/updater_test.go index 5759c9ba83..c88e646c5c 100644 --- a/pkg/config/updater_test.go +++ b/pkg/config/updater_test.go @@ -119,6 +119,41 @@ func TestUpdateDbConfig(t *testing.T) { }) } +func TestUpdateDbNetworkRestrictionsConfig(t *testing.T) { + server := "http://localhost" + client, err := v1API.NewClientWithResponses(server) + require.NoError(t, err) + + t.Run("skips update if disabled locally", func(t *testing.T) { + updater := NewConfigUpdater(*client) + // Run test + err := updater.UpdateDbNetworkRestrictionsConfig(context.Background(), "test-project", networkRestrictions{}) + // Check result + assert.NoError(t, err) + assert.False(t, gock.HasUnmatchedRequest()) + }) + + t.Run("returns error on 400 when enabled locally", func(t *testing.T) { + updater := NewConfigUpdater(*client) + // Setup mock server + defer gock.Off() + gock.New(server). + Get("/v1/projects/test-project/network-restrictions"). + Reply(http.StatusBadRequest). + JSON(map[string]any{ + "message": "project not allowed to set up network restrictions", + }) + // Run test + err := updater.UpdateDbNetworkRestrictionsConfig(context.Background(), "test-project", networkRestrictions{ + Enabled: true, + }) + // Check result + assert.Error(t, err) + assert.Contains(t, err.Error(), "unexpected status 400") + assert.True(t, gock.IsDone()) + }) +} + func TestUpdateExperimentalConfig(t *testing.T) { server := "http://localhost" client, err := v1API.NewClientWithResponses(server) diff --git a/pkg/function/deno.go b/pkg/function/deno.go index ec785c2467..6c98b367b7 100644 --- a/pkg/function/deno.go +++ b/pkg/function/deno.go @@ -111,7 +111,7 @@ func resolveHostPath(jsonPath, hostPath string) string { } // Ref: https://regex101.com/r/DfBdJA/1 -var importPathPattern = regexp.MustCompile(`(?i)(?:import|export)\s+(?:{[^{}]+}|.*?)\s*(?:from)?\s*['"](.*?)['"]|import\(\s*['"](.*?)['"]\)`) +var importPathPattern = regexp.MustCompile(`(?i)(?:import|export)\s+(?:type\s+)?(?:{[^{}]+}|.*?)\s*(?:from)?\s*['"](.*?)['"]|import\(\s*['"](.*?)['"]\)`) func (importMap *ImportMap) WalkImportPaths(srcPath string, readFile func(curr string, w io.Writer) error) error { seen := map[string]struct{}{} diff --git a/pkg/function/deno_test.go b/pkg/function/deno_test.go index ee3ac77956..aa9677f5ad 100644 --- a/pkg/function/deno_test.go +++ b/pkg/function/deno_test.go @@ -86,6 +86,21 @@ func TestImportPaths(t *testing.T) { assert.NoError(t, err) fsys.AssertExpectations(t) }) + + t.Run("iterates multiline import type statements", func(t *testing.T) { + // This test verifies that multiline import type statements are correctly parsed. + // The (?:type\s+)? group consumes the type keyword so braced imports hit the {[^{}]+} branch. + // Setup in-memory fs + fsys := MockFS{} + fsys.On("ReadFile", "testdata/modules/import_types.ts").Once() + fsys.On("ReadFile", "testdata/types/database.ts").Once() + // Run test + im := ImportMap{} + err := im.WalkImportPaths("testdata/modules/import_types.ts", fsys.ReadFile) + // Check error + assert.NoError(t, err) + fsys.AssertExpectations(t) + }) } func TestResolveImports(t *testing.T) { diff --git a/pkg/function/testdata/modules/import_types.ts b/pkg/function/testdata/modules/import_types.ts new file mode 100644 index 0000000000..d285ab0ee8 --- /dev/null +++ b/pkg/function/testdata/modules/import_types.ts @@ -0,0 +1,22 @@ +// Test file for multiline import type statements +// This pattern requires (?:type\s+)? to route braced imports into the {[^{}]+} branch + +// Multiline import type - should be matched by the regex +import type { + Database, + Json +} from '../types/database.ts' + +// Single line import type - should also work +import type { Database as DB } from '../types/database.ts' + +// Re-export type to verify export pattern +export type { Database } from '../types/database.ts' + +// Multiline export type +export type { + Json +} from '../types/database.ts' + +// Non-braced default type import - exercises the .*? branch on single-line +import type Database from '../types/database.ts' diff --git a/pkg/function/testdata/types/database.ts b/pkg/function/testdata/types/database.ts new file mode 100644 index 0000000000..4c49d8e2d0 --- /dev/null +++ b/pkg/function/testdata/types/database.ts @@ -0,0 +1,11 @@ +export type Database = { + public: { + Tables: { + users: { + Row: { id: string; name: string } + } + } + } +} + +export type Json = string | number | boolean | null | { [key: string]: Json } | Json[] diff --git a/pkg/go.mod b/pkg/go.mod index 1c2c3adab0..ec55d969bf 100644 --- a/pkg/go.mod +++ b/pkg/go.mod @@ -1,6 +1,6 @@ module github.com/supabase/cli/pkg -go 1.24.10 +go 1.25.0 require ( github.com/BurntSushi/toml v1.6.0 @@ -20,20 +20,20 @@ require ( github.com/jackc/pgx/v4 v4.18.3 github.com/joho/godotenv v1.5.1 github.com/oapi-codegen/nullable v1.1.0 - github.com/oapi-codegen/runtime v1.1.2 + github.com/oapi-codegen/runtime v1.2.0 github.com/spf13/afero v1.15.0 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 - github.com/tidwall/jsonc v0.3.2 - golang.org/x/mod v0.32.0 - google.golang.org/grpc v1.78.0 + github.com/tidwall/jsonc v0.3.3 + golang.org/x/mod v0.34.0 + google.golang.org/grpc v1.79.2 ) require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/ethereum/go-ethereum v1.16.8 // indirect + github.com/ethereum/go-ethereum v1.17.0 // 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 @@ -44,7 +44,6 @@ require ( github.com/lib/pq v1.10.9 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/cast v1.10.0 // indirect @@ -52,8 +51,8 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/go.sum b/pkg/go.sum index 3a2065560a..1611824c64 100644 --- a/pkg/go.sum +++ b/pkg/go.sum @@ -25,8 +25,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ecies/go/v2 v2.0.11 h1:xYhtMdLiqNi02oLirFmLyNbVXw6250h3WM6zJryQdiM= github.com/ecies/go/v2 v2.0.11/go.mod h1:LPRzoefP0Tam+1uesQOq3Gtb6M2OwlFUnXBTtBAKfDQ= -github.com/ethereum/go-ethereum v1.16.8 h1:LLLfkZWijhR5m6yrAXbdlTeXoqontH+Ga2f9igY7law= -github.com/ethereum/go-ethereum v1.16.8/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= +github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -132,8 +132,8 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= -github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= -github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -186,8 +186,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/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= -github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE= +github.com/tidwall/jsonc v0.3.3 h1:RVQqL3xFfDkKKXIDsrBiVQiEpBtxoKbmMXONb2H/y2w= +github.com/tidwall/jsonc v0.3.3/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -217,15 +217,15 @@ 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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -255,8 +255,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -272,8 +272,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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -290,8 +290,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.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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/file.go b/pkg/migration/file.go index fbd4a3b7fd..540c129e33 100644 --- a/pkg/migration/file.go +++ b/pkg/migration/file.go @@ -26,7 +26,10 @@ type MigrationFile struct { Statements []string } -var migrateFilePattern = regexp.MustCompile(`^([0-9]+)_(.*)\.sql$`) +var ( + migrateFilePattern = regexp.MustCompile(`^([0-9]+)_(.*)\.sql$`) + typeNamePattern = regexp.MustCompile(`type "([^"]+)" does not exist`) +) func NewMigrationFromFile(path string, fsys fs.FS) (*MigrationFile, error) { lines, err := parseFile(path, fsys) @@ -96,6 +99,14 @@ func (m *MigrationFile) ExecBatch(ctx context.Context, conn *pgx.Conn) error { if len(pgErr.Detail) > 0 { msg = append(msg, pgErr.Detail) } + // Provide helpful hint for extension type errors (SQLSTATE 42704: undefined_object) + if typeName := extractTypeName(pgErr.Message); len(typeName) > 0 && pgErr.Code == "42704" && !IsSchemaQualified(typeName) { + msg = append(msg, "") + msg = append(msg, "Hint: This type may be defined in a schema that's not in your search_path.") + msg = append(msg, " Use schema-qualified type references to avoid this error:") + msg = append(msg, fmt.Sprintf(" CREATE TABLE example (col extensions.%s);", typeName)) + msg = append(msg, " Learn more: supabase migration new --help") + } } msg = append(msg, fmt.Sprintf("At statement: %d", i), stat) return errors.Errorf("%w\n%s", err, strings.Join(msg, "\n")) @@ -120,6 +131,21 @@ func markError(stat string, pos int) string { return strings.Join(lines, "\n") } +// extractTypeName extracts the type name from PostgreSQL error messages like: +// 'type "ltree" does not exist' -> "ltree" +func extractTypeName(errMsg string) string { + matches := typeNamePattern.FindStringSubmatch(errMsg) + if len(matches) > 1 { + return matches[1] + } + return "" +} + +// IsSchemaQualified checks if a type name already contains a schema qualifier (e.g., "extensions.ltree") +func IsSchemaQualified(typeName string) bool { + return strings.Contains(typeName, ".") +} + func (m *MigrationFile) insertVersionSQL(conn *pgx.Conn, batch *pgconn.Batch) error { value := pgtype.TextArray{} if err := value.Set(m.Statements); err != nil { diff --git a/pkg/migration/file_test.go b/pkg/migration/file_test.go index 45bee71b65..703f26954c 100644 --- a/pkg/migration/file_test.go +++ b/pkg/migration/file_test.go @@ -77,4 +77,84 @@ func TestMigrationFile(t *testing.T) { assert.ErrorContains(t, err, "ERROR: schema \"public\" already exists (SQLSTATE 42P06)") assert.ErrorContains(t, err, "At statement: 0\ncreate schema public") }) + + t.Run("provides helpful hint for extension type errors", func(t *testing.T) { + migration := MigrationFile{ + Statements: []string{"CREATE TABLE test (path ltree NOT NULL)"}, + Version: "0", + } + // Setup mock postgres + conn := pgtest.NewConn() + defer conn.Close(t) + conn.Query(migration.Statements[0]). + ReplyError("42704", `type "ltree" does not exist`). + Query(INSERT_MIGRATION_VERSION, "0", "", migration.Statements). + Reply("INSERT 0 1") + // Run test + err := migration.ExecBatch(context.Background(), conn.MockClient(t)) + // Check error + assert.ErrorContains(t, err, `type "ltree" does not exist`) + assert.ErrorContains(t, err, "Hint: This type may be defined in a schema") + assert.ErrorContains(t, err, "extensions.ltree") + assert.ErrorContains(t, err, "supabase migration new --help") + assert.ErrorContains(t, err, "At statement: 0") + }) + + t.Run("skips hint for schema-qualified type errors", func(t *testing.T) { + migration := MigrationFile{ + Statements: []string{"CREATE TABLE test (path extensions.ltree NOT NULL)"}, + Version: "0", + } + // Setup mock postgres + conn := pgtest.NewConn() + defer conn.Close(t) + conn.Query(migration.Statements[0]). + ReplyError("42704", `type "extensions.ltree" does not exist`). + Query(INSERT_MIGRATION_VERSION, "0", "", migration.Statements). + Reply("INSERT 0 1") + // Run test + err := migration.ExecBatch(context.Background(), conn.MockClient(t)) + // Check error - should NOT contain hint since type is already schema-qualified + assert.ErrorContains(t, err, `type "extensions.ltree" does not exist`) + assert.NotContains(t, err.Error(), "Hint: This type may be defined in a schema") + }) +} + +func TestExtractTypeName(t *testing.T) { + t.Run("extracts type name from standard error message", func(t *testing.T) { + result := extractTypeName(`type "ltree" does not exist`) + assert.Equal(t, "ltree", result) + }) + + t.Run("extracts schema-qualified type name", func(t *testing.T) { + result := extractTypeName(`type "extensions.ltree" does not exist`) + assert.Equal(t, "extensions.ltree", result) + }) + + t.Run("extracts type with underscores", func(t *testing.T) { + result := extractTypeName(`type "my_custom_type" does not exist`) + assert.Equal(t, "my_custom_type", result) + }) + + t.Run("returns empty string for non-matching message", func(t *testing.T) { + result := extractTypeName(`column "name" does not exist`) + assert.Equal(t, "", result) + }) + + t.Run("returns empty string for empty message", func(t *testing.T) { + result := extractTypeName("") + assert.Equal(t, "", result) + }) + + t.Run("handles type names with numbers", func(t *testing.T) { + result := extractTypeName(`type "type123" does not exist`) + assert.Equal(t, "type123", result) + }) +} + +func TestIsSchemaQualified(t *testing.T) { + assert.True(t, IsSchemaQualified("extensions.ltree")) + assert.True(t, IsSchemaQualified("public.my_type")) + assert.False(t, IsSchemaQualified("ltree")) + assert.False(t, IsSchemaQualified("")) } diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000000..edc070840b --- /dev/null +++ b/tests/README.md @@ -0,0 +1,59 @@ +## Local e2e tests + +0. Install websocat + +```bash +brew install websocat +``` + +1. Start local stack + +```bash +supabase --workdir tests start +``` + +2. Run all tests + +```bash +./e2e-test.sh supabase +``` + +### Auth test + +1. Run user signup tests + +```bash +./tests/auth.sh +``` + +### PostgREST test + +1. Run RLS policy tests + +```bash +./tests/postgrest.sh +``` + +### Storage test + +1. Run storage bucket tests + +```bash +./tests/storage.sh +``` + +### Realtime test + +1. Join a room and broadcast + +```bash +./tests/realtime.sh +``` + +### Edge Function test + +1. Invoke hello-world function + +```bash +./tests/edge-runtime.sh +``` diff --git a/tests/auth.sh b/tests/auth.sh new file mode 100755 index 0000000000..17763e88f8 --- /dev/null +++ b/tests/auth.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -eou pipefail + +# 1. Create user with publishable key +output=$(curl -sS "$API_URL/auth/v1/signup" \ + -H "apikey: $PUBLISHABLE_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"email":"user@example.com","password":"aSecurePassword123"}' \ +) +if [[ $(echo "$output" | jq -r '.user.role') != 'authenticated' ]]; then + echo 'User sign up with publishable key should succeed.' >&2 + echo "$output" | jq + exit 1 +fi + +# 2. Delete user with secret key +user_id=$(echo "$output" | jq -r '.user.id') +output=$(curl -sS -X DELETE "$API_URL/auth/v1/admin/users/$user_id" \ + -H "apikey: $SECRET_KEY" \ + -H 'Content-Type: application/json' \ +) +if [[ "$output" != '{}' ]]; then + echo 'User deletion with secret key should succeed.' >&2 + echo "$output" | jq + exit 1 +fi + +# 3. Create user with legacy anon key +output=$(curl -sS "$API_URL/auth/v1/signup" \ + -H "Authorization: Bearer $ANON_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"email":"user@example.com","password":"aSecurePassword123"}' \ +) +if [[ $(echo "$output" | jq -r '.user.role') != 'authenticated' ]]; then + echo 'User sign up with legacy key should succeed.' >&2 + echo "$output" | jq + exit 1 +fi + +# 4. Delete user with legacy service role key +user_id=$(echo "$output" | jq -r '.user.id') +output=$(curl -sS -X DELETE "$API_URL/auth/v1/admin/users/$user_id" \ + -H "Authorization: Bearer $SERVICE_ROLE_KEY" \ + -H 'Content-Type: application/json' \ +) +if [[ "$output" != '{}' ]]; then + echo 'User deletion with legacy key should succeed.' >&2 + echo "$output" | jq + exit 1 +fi diff --git a/tests/edge-runtime.sh b/tests/edge-runtime.sh new file mode 100755 index 0000000000..25a8afbb60 --- /dev/null +++ b/tests/edge-runtime.sh @@ -0,0 +1,53 @@ +#!/bin/bash +set -eou pipefail + +# 0. Serve Edge Functions +# supabase --workdir tests functions serve + +# 1. POST request with publishable key +output=$(curl -sS "$API_URL/functions/v1/hello-world" \ + -H "apikey: $PUBLISHABLE_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"name":"Functions"}' \ +) +if [[ $(echo "$output" | jq -r '.message') != 'Hello Functions!' ]]; then + echo 'Edge Function request with publishable key should succeed.' >&2 + echo "$output" | jq + exit 1 +fi + +# 2. POST request with legacy key +output=$(curl -sS "$API_URL/functions/v1/hello-world" \ + -H "Authorization: Bearer $ANON_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"name":"Functions"}' \ +) +if [[ $(echo "$output" | jq -r '.message') != 'Hello Functions!' ]]; then + echo 'Edge Function request with anon key should succeed.' >&2 + echo "$output" | jq + exit 1 +fi + +# 3. POST request with secret key +output=$(curl -sS "$API_URL/functions/v1/hello-world" \ + -H "apikey: $SECRET_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"name":"Functions"}' \ +) +if [[ $(echo "$output" | jq -r '.message') != 'Hello Functions!' ]]; then + echo 'Edge Function request with secret key should succeed.' >&2 + echo "$output" | jq + exit 1 +fi + +# 4. POST request with service role key +output=$(curl -sS "$API_URL/functions/v1/hello-world" \ + -H "Authorization: Bearer $SERVICE_ROLE_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"name":"Functions"}' \ +) +if [[ $(echo "$output" | jq -r '.message') != 'Hello Functions!' ]]; then + echo 'Edge Function request with service role key should succeed.' >&2 + echo "$output" | jq + exit 1 +fi diff --git a/tests/postgrest.sh b/tests/postgrest.sh new file mode 100755 index 0000000000..5911c59544 --- /dev/null +++ b/tests/postgrest.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -eou pipefail + +# 0. Create test table with RLS +# supabase --workdir tests migrations up + +# 1. Create todo as service role +output=$(curl -sS "$API_URL/rest/v1/todos" \ + -H "apikey: $SECRET_KEY" \ + -H 'Content-Type: application/json' \ + -H 'Prefer: return=representation' \ + -d '{"task": "New task", "done": false}' \ +) +if [[ $(echo "$output" | jq -r 'length') != '1' ]]; then + echo 'Creating todo as service role should succeed.' >&2 + echo "$output" | jq + exit 1 +fi + +# 2. Create todo as anon role should fail +output=$(curl -sS "$API_URL/rest/v1/todos" \ + -H "apikey: $PUBLISHABLE_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"task": "New task", "done": false}' \ +) +if [[ $(echo "$output" | jq -r '.code') != '42501' ]]; then + echo 'Creating todo as anon role should fail.' >&2 + echo "$output" | jq + exit 1 +fi + +# 3. List todos as anon role +output=$(curl -sS -G "$API_URL/rest/v1/todos" \ + -H "apikey: $PUBLISHABLE_KEY" \ + -H 'Content-Type: application/json' \ +) +if [[ $(echo "$output" | jq -r 'length') != '1' ]]; then + echo 'Listing todos as anon role should succeed.' >&2 + echo "$output" | jq + exit 1 +fi + +# 4. Delete todo as anon role should fail +output=$(curl -sS -X DELETE "$API_URL/rest/v1/todos?id=eq.1" \ + -H "apikey: $PUBLISHABLE_KEY" \ + -H 'Content-Type: application/json' \ + -H 'Prefer: return=representation' \ +) +if [[ $(echo "$output" | jq -r 'length') != '0' ]]; then + echo 'Deleting todo as anon role should fail.' >&2 + echo "$output" | jq + exit 1 +fi + +# 5. Delete todo as authenticated role (custom jwt) +output=$(curl -sS -X DELETE "$API_URL/rest/v1/todos?id=not.eq.0" \ + -H "apikey: $PUBLISHABLE_KEY" \ + -H "Authorization: Bearer $SERVICE_ROLE_KEY" \ + -H 'Content-Type: application/json' \ + -H 'Prefer: return=representation' \ +) +if [[ $(echo "$output" | jq -r 'length') != '1' ]]; then + echo 'Deleting todo as authenticated role should succeed.' >&2 + echo "$output" | jq + exit 1 +fi diff --git a/tests/realtime.sh b/tests/realtime.sh new file mode 100755 index 0000000000..512bc009ce --- /dev/null +++ b/tests/realtime.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -eou pipefail + +# 1. Join realtime with legacy key +output=$(echo '{"topic":"realtime:room1","ref":1,"event":"phx_join","payload":{"config":{"broadcast":{"ack":true},"presence":{"enabled":true},"private":false}}}' |\ + websocat "ws://127.0.0.1:54321/realtime/v1/websocket?apikey=$ANON_KEY&vsn=1.0.0" \ +) +if [[ $(echo "$output" | jq -r '.payload.status') != 'ok' ]]; then + echo 'Joining realtime with legacy key should succeed.' >&2 + echo "$output" | jq + exit 1 +fi + +# 2. Join realtime with publishable key +output=$(echo '{"topic":"realtime:room1","ref":1,"event":"phx_join","payload":{"config":{"broadcast":{"ack":true},"presence":{"enabled":true},"private":false}}}' |\ + websocat "ws://127.0.0.1:54321/realtime/v1/websocket?apikey=$PUBLISHABLE_KEY&vsn=1.0.0" \ +) +if [[ $(echo "$output" | jq -r '.payload.status') != 'ok' ]]; then + echo 'Joining realtime as anon should succeed.' >&2 + echo "$output" | jq + exit 1 +fi + +## 3. Broadcast with secret key +curl -sSf "$API_URL/realtime/v1/api/broadcast" \ + -H "apikey: $SECRET_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"messages":[{"topic":"room1","event":"my_event","payload":{"foo":"bar"},"private":true}]}' diff --git a/tests/storage.sh b/tests/storage.sh new file mode 100755 index 0000000000..1a68fc8102 --- /dev/null +++ b/tests/storage.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -eou pipefail + +# 1. Create test bucket as service role +output=$(curl -sS "$API_URL/storage/v1/bucket" \ + -H "apikey: $SECRET_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"name":"test"}' \ +) +if [[ $(echo "$output" | jq -r '.name') != 'test' ]]; then + echo 'Creating storage bucket as service role should succeed.' >&2 + echo "$output" | jq + exit 1 +fi + +# 2. Create test bucket as service role (legacy key) +output=$(curl -sS -X DELETE "$API_URL/storage/v1/bucket/test" \ + -H "apikey: $SERVICE_ROLE_KEY" \ + -H 'Content-Type: application/json' \ +) +if [[ $(echo "$output" | jq -r '.message') != 'Successfully deleted' ]]; then + echo 'Deleting storage bucket as service role should succeed.' >&2 + echo "$output" | jq + exit 1 +fi + +# 3. Unauthenticated requests are rejected +output=$(curl -sS "$API_URL/storage/v1/bucket" \ + -H 'Content-Type: application/json' \ + -d '{"name":"test"}' \ +) +if [[ $(echo "$output" | jq -r '.error') != 'Unauthorized' ]]; then + echo 'Unauthenticated requests should be rejected.' >&2 + echo "$output" | jq + exit 1 +fi diff --git a/tests/supabase/.gitignore b/tests/supabase/.gitignore new file mode 100644 index 0000000000..a735017e0d --- /dev/null +++ b/tests/supabase/.gitignore @@ -0,0 +1,13 @@ +# Supabase +.branches +.temp +.env + +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/tests/supabase/functions/hello-world/.npmrc b/tests/supabase/functions/hello-world/.npmrc new file mode 100644 index 0000000000..48c6388638 --- /dev/null +++ b/tests/supabase/functions/hello-world/.npmrc @@ -0,0 +1,3 @@ +# Configuration for private npm package dependencies +# For more information on using private registries with Edge Functions, see: +# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries diff --git a/tests/supabase/functions/hello-world/deno.json b/tests/supabase/functions/hello-world/deno.json new file mode 100644 index 0000000000..758d0703d1 --- /dev/null +++ b/tests/supabase/functions/hello-world/deno.json @@ -0,0 +1,5 @@ +{ + "imports": { + "@supabase/functions-js": "jsr:@supabase/functions-js@^2" + } +} diff --git a/tests/supabase/functions/hello-world/index.ts b/tests/supabase/functions/hello-world/index.ts new file mode 100644 index 0000000000..d928d9b5bb --- /dev/null +++ b/tests/supabase/functions/hello-world/index.ts @@ -0,0 +1,32 @@ +// Follow this setup guide to integrate the Deno language server with your editor: +// https://deno.land/manual/getting_started/setup_your_environment +// This enables autocomplete, go to definition, etc. + +// Setup type definitions for built-in Supabase Runtime APIs +import "@supabase/functions-js/edge-runtime.d.ts" + +console.log("Hello from Functions!") + +Deno.serve(async (req) => { + const { name } = await req.json() + const data = { + message: `Hello ${name}!`, + } + + return new Response( + JSON.stringify(data), + { headers: { "Content-Type": "application/json" } }, + ) +}) + +/* To invoke locally: + + 1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) + 2. Make an HTTP request: + + curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/hello-world' \ + --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ + --header 'Content-Type: application/json' \ + --data '{"name":"Functions"}' + +*/ diff --git a/tests/supabase/migrations/0_create_todos_table.sql b/tests/supabase/migrations/0_create_todos_table.sql new file mode 100644 index 0000000000..46a601433d --- /dev/null +++ b/tests/supabase/migrations/0_create_todos_table.sql @@ -0,0 +1,21 @@ +-- Create a test table +CREATE TABLE IF NOT EXISTS public.todos ( + id SERIAL PRIMARY KEY, + task TEXT NOT NULL, + done BOOLEAN DEFAULT false, + created_at TIMESTAMPTZ DEFAULT now() +); + +-- Enable RLS +ALTER TABLE public.todos ENABLE ROW LEVEL SECURITY; + +-- Allow anon to read all todos +CREATE POLICY "Allow anon read" ON public.todos FOR SELECT TO anon USING (true); + +-- Allow authenticated users full access +CREATE POLICY "Allow authenticated full access" ON public.todos FOR ALL TO authenticated USING (true) WITH CHECK (true); + +-- Grant permissions +GRANT SELECT ON public.todos TO anon; +GRANT ALL ON public.todos TO authenticated; +GRANT USAGE, SELECT ON SEQUENCE public.todos_id_seq TO authenticated; diff --git a/tools/jsonschema/main.go b/tools/jsonschema/main.go new file mode 100644 index 0000000000..507ae338e5 --- /dev/null +++ b/tools/jsonschema/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + + "github.com/google/jsonschema-go/jsonschema" + "github.com/supabase/cli/pkg/config" +) + +func main() { + if err := run(); err != nil { + panic(err) + } +} + +func run() error { + secret, err := jsonschema.For[config.Secret](&jsonschema.ForOptions{}) + if err != nil { + return err + } + secretSchema, err := json.Marshal(secret) + if err != nil { + return err + } + // Replace secret schema because TypeSchemas is not working + opts := &jsonschema.ForOptions{ + TypeSchemas: map[reflect.Type]*jsonschema.Schema{ + reflect.TypeFor[config.Secret](): {Type: "string"}, + }, + } + js, err := jsonschema.For[config.Config](opts) + if err != nil { + return err + } + data, err := json.Marshal(js) + if err != nil { + return err + } + result := bytes.ReplaceAll(data, secretSchema, []byte(`{"type":"string"}`)) + _, err = fmt.Println(string(result)) + return err +}