github.com/supabase/cli@v1.168.1/internal/db/start/start.go (about)

     1  package start
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/docker/docker/api/types/container"
    14  	"github.com/docker/docker/api/types/network"
    15  	"github.com/docker/docker/client"
    16  	"github.com/docker/go-connections/nat"
    17  	"github.com/go-errors/errors"
    18  	"github.com/jackc/pgconn"
    19  	"github.com/jackc/pgx/v4"
    20  	"github.com/spf13/afero"
    21  	"github.com/supabase/cli/internal/db/push"
    22  	"github.com/supabase/cli/internal/migration/apply"
    23  	"github.com/supabase/cli/internal/status"
    24  	"github.com/supabase/cli/internal/utils"
    25  )
    26  
    27  var (
    28  	ErrDatabase   = errors.New("database is not healthy")
    29  	HealthTimeout = 120 * time.Second
    30  	//go:embed templates/schema.sql
    31  	initialSchema string
    32  )
    33  
    34  func Run(ctx context.Context, fsys afero.Fs) error {
    35  	if err := utils.LoadConfigFS(fsys); err != nil {
    36  		return err
    37  	}
    38  	if err := utils.AssertSupabaseDbIsRunning(); err == nil {
    39  		fmt.Fprintln(os.Stderr, "Postgres database is already running.")
    40  		return nil
    41  	} else if !errors.Is(err, utils.ErrNotRunning) {
    42  		return err
    43  	}
    44  	// Skip logflare container in db start
    45  	utils.Config.Analytics.Enabled = false
    46  	err := StartDatabase(ctx, fsys, os.Stderr)
    47  	if err != nil {
    48  		if err := utils.DockerRemoveAll(context.Background(), io.Discard); err != nil {
    49  			fmt.Fprintln(os.Stderr, err)
    50  		}
    51  	}
    52  	return err
    53  }
    54  
    55  func NewContainerConfig() container.Config {
    56  	env := []string{
    57  		"POSTGRES_PASSWORD=" + utils.Config.Db.Password,
    58  		"POSTGRES_HOST=/var/run/postgresql",
    59  		"POSTGRES_INITDB_ARGS=--lc-ctype=C.UTF-8",
    60  		"JWT_SECRET=" + utils.Config.Auth.JwtSecret,
    61  		fmt.Sprintf("JWT_EXP=%d", utils.Config.Auth.JwtExpiry),
    62  	}
    63  	if len(utils.Config.Experimental.OrioleDBVersion) > 0 {
    64  		env = append(env,
    65  			"POSTGRES_INITDB_ARGS=--lc-collate=C",
    66  			fmt.Sprintf("S3_ENABLED=%t", true),
    67  			"S3_HOST="+utils.Config.Experimental.S3Host,
    68  			"S3_REGION="+utils.Config.Experimental.S3Region,
    69  			"S3_ACCESS_KEY="+utils.Config.Experimental.S3AccessKey,
    70  			"S3_SECRET_KEY="+utils.Config.Experimental.S3SecretKey,
    71  		)
    72  	} else {
    73  		env = append(env, "POSTGRES_INITDB_ARGS=--lc-collate=C.UTF-8")
    74  	}
    75  	config := container.Config{
    76  		Image: utils.Config.Db.Image,
    77  		Env:   env,
    78  		Healthcheck: &container.HealthConfig{
    79  			Test:     []string{"CMD", "pg_isready", "-U", "postgres", "-h", "127.0.0.1", "-p", "5432"},
    80  			Interval: 10 * time.Second,
    81  			Timeout:  2 * time.Second,
    82  			Retries:  3,
    83  		},
    84  		Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /etc/postgresql.schema.sql && cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && docker-entrypoint.sh postgres -D /etc/postgresql
    85  ` + initialSchema + `
    86  EOF
    87  ` + utils.Config.Db.RootKey + `
    88  EOF
    89  `},
    90  	}
    91  	if utils.Config.Db.MajorVersion >= 14 {
    92  		config.Cmd = []string{"postgres",
    93  			"-c", "config_file=/etc/postgresql/postgresql.conf",
    94  			// Ref: https://postgrespro.com/list/thread-id/2448092
    95  			"-c", `search_path="$user",public,extensions`,
    96  		}
    97  	}
    98  	return config
    99  }
   100  
   101  func NewHostConfig() container.HostConfig {
   102  	hostPort := strconv.FormatUint(uint64(utils.Config.Db.Port), 10)
   103  	hostConfig := WithSyslogConfig(container.HostConfig{
   104  		PortBindings:  nat.PortMap{"5432/tcp": []nat.PortBinding{{HostPort: hostPort}}},
   105  		RestartPolicy: container.RestartPolicy{Name: "always"},
   106  		Binds: []string{
   107  			utils.DbId + ":/var/lib/postgresql/data",
   108  			utils.ConfigId + ":/etc/postgresql-custom",
   109  		},
   110  		ExtraHosts: []string{"host.docker.internal:host-gateway"},
   111  	})
   112  	return hostConfig
   113  }
   114  
   115  func StartDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error {
   116  	config := NewContainerConfig()
   117  	hostConfig := NewHostConfig()
   118  	networkingConfig := network.NetworkingConfig{
   119  		EndpointsConfig: map[string]*network.EndpointSettings{
   120  			utils.NetId: {
   121  				Aliases: utils.DbAliases,
   122  			},
   123  		},
   124  	}
   125  	if utils.Config.Db.MajorVersion <= 14 {
   126  		config.Entrypoint = nil
   127  		hostConfig.Tmpfs = map[string]string{"/docker-entrypoint-initdb.d": ""}
   128  	}
   129  	// Creating volume will not override existing volume, so we must inspect explicitly
   130  	_, err := utils.Docker.VolumeInspect(ctx, utils.DbId)
   131  	utils.NoBackupVolume = client.IsErrNotFound(err)
   132  	if utils.NoBackupVolume {
   133  		fmt.Fprintln(w, "Starting database...")
   134  	} else {
   135  		fmt.Fprintln(w, "Starting database from backup...")
   136  	}
   137  	if _, err := utils.DockerStart(ctx, config, hostConfig, networkingConfig, utils.DbId); err != nil {
   138  		return err
   139  	}
   140  	if !WaitForHealthyService(ctx, utils.DbId, HealthTimeout) {
   141  		return errors.New(ErrDatabase)
   142  	}
   143  	// Initialize if we are on PG14 and there's no existing db volume
   144  	if utils.NoBackupVolume {
   145  		if err := setupDatabase(ctx, fsys, w, options...); err != nil {
   146  			return err
   147  		}
   148  	}
   149  	return initCurrentBranch(fsys)
   150  }
   151  
   152  func RetryEverySecond(ctx context.Context, callback func() bool, timeout time.Duration) bool {
   153  	now := time.Now()
   154  	expiry := now.Add(timeout)
   155  	ticker := time.NewTicker(time.Second)
   156  	defer ticker.Stop()
   157  	for t := now; t.Before(expiry) && ctx.Err() == nil; t = <-ticker.C {
   158  		if callback() {
   159  			return true
   160  		}
   161  	}
   162  	return false
   163  }
   164  
   165  func WaitForHealthyService(ctx context.Context, container string, timeout time.Duration) bool {
   166  	probe := func() bool {
   167  		return status.AssertContainerHealthy(ctx, container) == nil
   168  	}
   169  	return RetryEverySecond(ctx, probe, timeout)
   170  }
   171  
   172  func WithSyslogConfig(hostConfig container.HostConfig) container.HostConfig {
   173  	if utils.Config.Analytics.Enabled {
   174  		hostConfig.LogConfig.Type = "syslog"
   175  		hostConfig.LogConfig.Config = map[string]string{
   176  			"syslog-address": fmt.Sprintf("tcp://%s:%d", utils.Config.Hostname, utils.Config.Analytics.VectorPort),
   177  			"tag":            "{{.Name}}",
   178  		}
   179  	}
   180  	return hostConfig
   181  }
   182  
   183  func initCurrentBranch(fsys afero.Fs) error {
   184  	// Create _current_branch file to avoid breaking db branch commands
   185  	if _, err := fsys.Stat(utils.CurrBranchPath); err == nil {
   186  		return nil
   187  	} else if !errors.Is(err, os.ErrNotExist) {
   188  		return errors.Errorf("failed init current branch: %w", err)
   189  	}
   190  	return utils.WriteFile(utils.CurrBranchPath, []byte("main"), fsys)
   191  }
   192  
   193  func initSchema(ctx context.Context, conn *pgx.Conn, host string, w io.Writer) error {
   194  	fmt.Fprintln(w, "Setting up initial schema...")
   195  	if utils.Config.Db.MajorVersion <= 14 {
   196  		return initSchema14(ctx, conn)
   197  	}
   198  	return initSchema15(ctx, host)
   199  }
   200  
   201  func initSchema14(ctx context.Context, conn *pgx.Conn) error {
   202  	if err := apply.BatchExecDDL(ctx, conn, strings.NewReader(utils.GlobalsSql)); err != nil {
   203  		return err
   204  	}
   205  	return apply.BatchExecDDL(ctx, conn, strings.NewReader(utils.InitialSchemaSql))
   206  }
   207  
   208  func initSchema15(ctx context.Context, host string) error {
   209  	// Apply service migrations
   210  	logger := utils.GetDebugLogger()
   211  	if err := utils.DockerRunOnceWithStream(ctx, utils.RealtimeImage, []string{
   212  		"PORT=4000",
   213  		"DB_HOST=" + host,
   214  		"DB_PORT=5432",
   215  		"DB_USER=supabase_admin",
   216  		"DB_PASSWORD=" + utils.Config.Db.Password,
   217  		"DB_NAME=postgres",
   218  		"DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime",
   219  		"DB_ENC_KEY=" + utils.Config.Realtime.EncryptionKey,
   220  		"API_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
   221  		"METRICS_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
   222  		"FLY_APP_NAME=realtime",
   223  		"SECRET_KEY_BASE=" + utils.Config.Realtime.SecretKeyBase,
   224  		"ERL_AFLAGS=" + utils.ToRealtimeEnv(utils.Config.Realtime.IpVersion),
   225  		"ENABLE_TAILSCALE=false",
   226  		"DNS_NODES=''",
   227  		"RLIMIT_NOFILE=",
   228  		fmt.Sprintf("MAX_HEADER_LENGTH=%d", utils.Config.Realtime.MaxHeaderLength),
   229  	}, []string{"/bin/sh", "-c", fmt.Sprintf(
   230  		`/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo); Application.ensure_all_started(:realtime); Realtime.Tenants.health_check("%s")'`,
   231  		utils.Config.Realtime.TenantId,
   232  	)}, io.Discard, logger); err != nil {
   233  		return err
   234  	}
   235  	if err := utils.DockerRunOnceWithStream(ctx, utils.Config.Storage.Image, []string{
   236  		"DB_INSTALL_ROLES=false",
   237  		"ANON_KEY=" + utils.Config.Auth.AnonKey,
   238  		"SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey,
   239  		"PGRST_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
   240  		fmt.Sprintf("DATABASE_URL=postgresql://supabase_storage_admin:%s@%s:5432/postgres", utils.Config.Db.Password, host),
   241  		fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit),
   242  		"STORAGE_BACKEND=file",
   243  		"TENANT_ID=stub",
   244  		// TODO: https://github.com/supabase/storage-api/issues/55
   245  		"REGION=stub",
   246  		"GLOBAL_S3_BUCKET=stub",
   247  	}, []string{"node", "dist/scripts/migrate-call.js"}, io.Discard, logger); err != nil {
   248  		return err
   249  	}
   250  	return utils.DockerRunOnceWithStream(ctx, utils.Config.Auth.Image, []string{
   251  		fmt.Sprintf("API_EXTERNAL_URL=http://%s:%d", host, utils.Config.Api.Port),
   252  		"GOTRUE_LOG_LEVEL=error",
   253  		"GOTRUE_DB_DRIVER=postgres",
   254  		fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:5432/postgres", utils.Config.Db.Password, host),
   255  		"GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl,
   256  		"GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
   257  	}, []string{"gotrue", "migrate"}, io.Discard, logger)
   258  }
   259  
   260  func setupDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error {
   261  	conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{}, options...)
   262  	if err != nil {
   263  		return err
   264  	}
   265  	defer conn.Close(context.Background())
   266  	if err := SetupDatabase(ctx, conn, utils.DbId, w, fsys); err != nil {
   267  		return err
   268  	}
   269  	return apply.MigrateAndSeed(ctx, "", conn, fsys)
   270  }
   271  
   272  func SetupDatabase(ctx context.Context, conn *pgx.Conn, host string, w io.Writer, fsys afero.Fs) error {
   273  	if err := initSchema(ctx, conn, host, w); err != nil {
   274  		return err
   275  	}
   276  	return push.CreateCustomRoles(ctx, conn, os.Stderr, fsys)
   277  }