supabase-cli/internal/start/start.go

664 lines
23 KiB
Go

package start
import (
"bytes"
"context"
_ "embed"
"fmt"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"text/template"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/go-connections/nat"
"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/db/start"
"github.com/supabase/cli/internal/seed/buckets"
"github.com/supabase/cli/internal/services"
"github.com/supabase/cli/internal/status"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/pkg/config"
"golang.org/x/mod/semver"
)
func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignoreHealthCheck bool) error {
// Sanity checks.
{
if err := flags.LoadConfig(fsys); err != nil {
return err
}
if err := utils.AssertSupabaseDbIsRunning(); err == nil {
fmt.Fprintln(os.Stderr, utils.Aqua("supabase start")+" is already running.")
names := status.CustomName{}
return status.Run(ctx, names, utils.OutputPretty, fsys)
} else if !errors.Is(err, utils.ErrNotRunning) {
return err
}
if err := flags.LoadProjectRef(fsys); err == nil {
_ = services.CheckVersions(ctx, fsys)
}
}
if err := utils.RunProgram(ctx, func(p utils.Program, ctx context.Context) error {
dbConfig := pgconn.Config{
Host: utils.DbId,
Port: 5432,
User: "postgres",
Password: utils.Config.Db.Password,
Database: "postgres",
}
return run(p, ctx, fsys, excludedContainers, dbConfig)
}); err != nil {
if ignoreHealthCheck && start.IsUnhealthyError(err) {
fmt.Fprintln(os.Stderr, err)
} else {
if err := utils.DockerRemoveAll(context.Background(), os.Stderr, utils.Config.ProjectId); err != nil {
fmt.Fprintln(os.Stderr, err)
}
return err
}
}
fmt.Fprintf(os.Stderr, "Started %s local development setup.\n\n", utils.Aqua("supabase"))
status.PrettyPrint(os.Stdout, excludedContainers...)
return nil
}
type kongConfig struct {
GotrueId string
RestId string
RealtimeId string
StorageId string
PgmetaId string
EdgeRuntimeId string
LogflareId string
PoolerId string
ApiHost string
ApiPort uint16
}
// TODO: deprecate after removing storage headers from kong
func StorageVersionBelow(target string) bool {
parts := strings.Split(utils.Config.Storage.Image, ":v")
return semver.Compare(parts[len(parts)-1], target) < 0
}
var (
//go:embed templates/kong.yml
kongConfigEmbed string
kongConfigTemplate = template.Must(template.New("kongConfig").Funcs(template.FuncMap{
"StorageVersionBelow": StorageVersionBelow,
}).Parse(kongConfigEmbed))
//go:embed templates/custom_nginx.template
nginxConfigEmbed string
// Hardcoded configs which match nginxConfigEmbed
nginxEmailTemplateDir = "/home/kong/templates/email"
nginxTemplateServerPort = 8088
)
type vectorConfig struct {
ApiKey string
VectorId string
LogflareId string
KongId string
GotrueId string
RestId string
RealtimeId string
StorageId string
EdgeRuntimeId string
DbId string
}
var (
//go:embed templates/vector.yaml
vectorConfigEmbed string
vectorConfigTemplate = template.Must(template.New("vectorConfig").Parse(vectorConfigEmbed))
)
type poolerTenant struct {
DbHost string
DbPort uint16
DbDatabase string
DbPassword string
ExternalId string
ModeType config.PoolMode
DefaultMaxClients uint
DefaultPoolSize uint
}
var (
//go:embed templates/pooler.exs
poolerTenantEmbed string
poolerTenantTemplate = template.Must(template.New("poolerTenant").Parse(poolerTenantEmbed))
)
var serviceTimeout = 30 * time.Second
func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers []string, dbConfig pgconn.Config, options ...func(*pgx.ConnConfig)) error {
excluded := make(map[string]bool)
for _, name := range excludedContainers {
excluded[name] = true
}
jwks, err := utils.Config.Auth.ResolveJWKS(ctx)
if err != nil {
return err
}
// Start Postgres.
w := utils.StatusWriter{Program: p}
if dbConfig.Host == utils.DbId {
if err := start.StartDatabase(ctx, "", fsys, w, options...); err != nil {
return err
}
}
var started []string
var isStorageEnabled = utils.Config.Storage.Enabled && !isContainerExcluded(utils.Config.Storage.Image, excluded)
var isImgProxyEnabled = utils.Config.Storage.ImageTransformation != nil &&
utils.Config.Storage.ImageTransformation.Enabled && !isContainerExcluded(utils.Config.Storage.ImgProxyImage, excluded)
p.Send(utils.StatusMsg("Starting containers..."))
// Start Kong.
if !isContainerExcluded(utils.Config.Api.KongImage, excluded) {
var kongConfigBuf bytes.Buffer
if err := kongConfigTemplate.Option("missingkey=error").Execute(&kongConfigBuf, kongConfig{
GotrueId: utils.GotrueId,
RestId: utils.RestId,
RealtimeId: utils.Config.Realtime.TenantId,
StorageId: utils.StorageId,
PgmetaId: utils.PgmetaId,
EdgeRuntimeId: utils.EdgeRuntimeId,
LogflareId: utils.LogflareId,
PoolerId: utils.PoolerId,
ApiHost: utils.Config.Hostname,
ApiPort: utils.Config.Api.Port,
}); err != nil {
return errors.Errorf("failed to exec template: %w", err)
}
binds := []string{}
for id, tmpl := range utils.Config.Auth.Email.Template {
if len(tmpl.ContentPath) == 0 {
continue
}
hostPath := tmpl.ContentPath
if !filepath.IsAbs(tmpl.ContentPath) {
var err error
hostPath, err = filepath.Abs(hostPath)
if err != nil {
return errors.Errorf("failed to resolve absolute path: %w", err)
}
}
dockerPath := path.Join(nginxEmailTemplateDir, id+filepath.Ext(hostPath))
binds = append(binds, fmt.Sprintf("%s:%s:rw", hostPath, dockerPath))
}
dockerPort := uint16(8000)
if utils.Config.Api.Tls.Enabled {
dockerPort = 8443
}
if _, err := utils.DockerStart(
ctx,
container.Config{
Image: utils.Config.Api.KongImage,
Env: []string{
"KONG_DATABASE=off",
"KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml",
"KONG_DNS_ORDER=LAST,A,CNAME", // https://github.com/supabase/cli/issues/14
"KONG_PLUGINS=request-transformer,cors",
fmt.Sprintf("KONG_PORT_MAPS=%d:8000", utils.Config.Api.Port),
// Need to increase the nginx buffers in kong to avoid it rejecting the rather
// sizeable response headers azure can generate
// Ref: https://github.com/Kong/kong/issues/3974#issuecomment-482105126
"KONG_NGINX_PROXY_PROXY_BUFFER_SIZE=160k",
"KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k",
"KONG_NGINX_WORKER_PROCESSES=1",
// Use modern TLS certificate
"KONG_SSL_CERT=/home/kong/localhost.crt",
"KONG_SSL_CERT_KEY=/home/kong/localhost.key",
},
Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /home/kong/kong.yml && \
cat <<'EOF' > /home/kong/custom_nginx.template && \
cat <<'EOF' > /home/kong/localhost.crt && \
cat <<'EOF' > /home/kong/localhost.key && \
./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template
` + kongConfigBuf.String() + `
EOF
` + nginxConfigEmbed + `
EOF
` + status.KongCert + `
EOF
` + status.KongKey + `
EOF
`},
ExposedPorts: nat.PortSet{
"8000/tcp": {},
"8443/tcp": {},
nat.Port(fmt.Sprintf("%d/tcp", nginxTemplateServerPort)): {},
},
},
container.HostConfig{
Binds: binds,
PortBindings: nat.PortMap{nat.Port(fmt.Sprintf("%d/tcp", dockerPort)): []nat.PortBinding{{
HostPort: strconv.FormatUint(uint64(utils.Config.Api.Port), 10)},
}},
RestartPolicy: container.RestartPolicy{Name: "always"},
},
network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
utils.NetId: {
Aliases: utils.KongAliases,
},
},
},
utils.KongId,
); err != nil {
return err
}
started = append(started, utils.KongId)
}
// Start GoTrue.
if utils.Config.Auth.Enabled && !isContainerExcluded(utils.Config.Auth.Image, excluded) {
var testOTP bytes.Buffer
if len(utils.Config.Auth.Sms.TestOTP) > 0 {
formatMapForEnvConfig(utils.Config.Auth.Sms.TestOTP, &testOTP)
}
env := []string{
"API_EXTERNAL_URL=" + utils.Config.Api.ExternalUrl,
"GOTRUE_API_HOST=0.0.0.0",
"GOTRUE_API_PORT=9999",
"GOTRUE_DB_DRIVER=postgres",
fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
"GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl,
"GOTRUE_URI_ALLOW_LIST=" + strings.Join(utils.Config.Auth.AdditionalRedirectUrls, ","),
fmt.Sprintf("GOTRUE_DISABLE_SIGNUP=%v", !utils.Config.Auth.EnableSignup),
"GOTRUE_JWT_ADMIN_ROLES=service_role",
"GOTRUE_JWT_AUD=authenticated",
"GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated",
fmt.Sprintf("GOTRUE_JWT_EXP=%v", utils.Config.Auth.JwtExpiry),
"GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
"GOTRUE_JWT_ISSUER=" + utils.GetApiUrl("/auth/v1"),
fmt.Sprintf("GOTRUE_EXTERNAL_EMAIL_ENABLED=%v", utils.Config.Auth.Email.EnableSignup),
fmt.Sprintf("GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=%v", utils.Config.Auth.Email.DoubleConfirmChanges),
fmt.Sprintf("GOTRUE_MAILER_AUTOCONFIRM=%v", !utils.Config.Auth.Email.EnableConfirmations),
fmt.Sprintf("GOTRUE_MAILER_OTP_LENGTH=%v", utils.Config.Auth.Email.OtpLength),
fmt.Sprintf("GOTRUE_MAILER_OTP_EXP=%v", utils.Config.Auth.Email.OtpExpiry),
fmt.Sprintf("GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=%v", utils.Config.Auth.EnableAnonymousSignIns),
fmt.Sprintf("GOTRUE_SMTP_MAX_FREQUENCY=%v", utils.Config.Auth.Email.MaxFrequency),
"GOTRUE_MAILER_URLPATHS_INVITE=" + utils.GetApiUrl("/auth/v1/verify"),
"GOTRUE_MAILER_URLPATHS_CONFIRMATION=" + utils.GetApiUrl("/auth/v1/verify"),
"GOTRUE_MAILER_URLPATHS_RECOVERY=" + utils.GetApiUrl("/auth/v1/verify"),
"GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=" + utils.GetApiUrl("/auth/v1/verify"),
"GOTRUE_RATE_LIMIT_EMAIL_SENT=360000",
fmt.Sprintf("GOTRUE_EXTERNAL_PHONE_ENABLED=%v", utils.Config.Auth.Sms.EnableSignup),
fmt.Sprintf("GOTRUE_SMS_AUTOCONFIRM=%v", !utils.Config.Auth.Sms.EnableConfirmations),
fmt.Sprintf("GOTRUE_SMS_MAX_FREQUENCY=%v", utils.Config.Auth.Sms.MaxFrequency),
"GOTRUE_SMS_OTP_EXP=6000",
"GOTRUE_SMS_OTP_LENGTH=6",
fmt.Sprintf("GOTRUE_SMS_TEMPLATE=%v", utils.Config.Auth.Sms.Template),
"GOTRUE_SMS_TEST_OTP=" + testOTP.String(),
fmt.Sprintf("GOTRUE_PASSWORD_MIN_LENGTH=%v", utils.Config.Auth.MinimumPasswordLength),
fmt.Sprintf("GOTRUE_PASSWORD_REQUIRED_CHARACTERS=%v", utils.Config.Auth.PasswordRequirements.ToChar()),
fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=%v", utils.Config.Auth.EnableRefreshTokenRotation),
fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL=%v", utils.Config.Auth.RefreshTokenReuseInterval),
fmt.Sprintf("GOTRUE_SECURITY_MANUAL_LINKING_ENABLED=%v", utils.Config.Auth.EnableManualLinking),
fmt.Sprintf("GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION=%v", utils.Config.Auth.Email.SecurePasswordChange),
fmt.Sprintf("GOTRUE_MFA_PHONE_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.Phone.EnrollEnabled),
fmt.Sprintf("GOTRUE_MFA_PHONE_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.Phone.VerifyEnabled),
fmt.Sprintf("GOTRUE_MFA_TOTP_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.TOTP.EnrollEnabled),
fmt.Sprintf("GOTRUE_MFA_TOTP_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.TOTP.VerifyEnabled),
fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.EnrollEnabled),
fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.VerifyEnabled),
fmt.Sprintf("GOTRUE_MFA_MAX_ENROLLED_FACTORS=%v", utils.Config.Auth.MFA.MaxEnrolledFactors),
}
if utils.Config.Auth.Email.Smtp != nil && utils.Config.Auth.Email.Smtp.Enabled {
env = append(env,
fmt.Sprintf("GOTRUE_SMTP_HOST=%s", utils.Config.Auth.Email.Smtp.Host),
fmt.Sprintf("GOTRUE_SMTP_PORT=%d", utils.Config.Auth.Email.Smtp.Port),
fmt.Sprintf("GOTRUE_SMTP_USER=%s", utils.Config.Auth.Email.Smtp.User),
fmt.Sprintf("GOTRUE_SMTP_PASS=%s", utils.Config.Auth.Email.Smtp.Pass.Value),
fmt.Sprintf("GOTRUE_SMTP_ADMIN_EMAIL=%s", utils.Config.Auth.Email.Smtp.AdminEmail),
fmt.Sprintf("GOTRUE_SMTP_SENDER_NAME=%s", utils.Config.Auth.Email.Smtp.SenderName),
)
} else if utils.Config.Inbucket.Enabled {
env = append(env,
"GOTRUE_SMTP_HOST="+utils.InbucketId,
"GOTRUE_SMTP_PORT=2500",
fmt.Sprintf("GOTRUE_SMTP_ADMIN_EMAIL=%s", utils.Config.Inbucket.AdminEmail),
fmt.Sprintf("GOTRUE_SMTP_SENDER_NAME=%s", utils.Config.Inbucket.SenderName),
)
}
if utils.Config.Auth.Sessions.Timebox > 0 {
env = append(env, fmt.Sprintf("GOTRUE_SESSIONS_TIMEBOX=%v", utils.Config.Auth.Sessions.Timebox))
}
if utils.Config.Auth.Sessions.InactivityTimeout > 0 {
env = append(env, fmt.Sprintf("GOTRUE_SESSIONS_INACTIVITY_TIMEOUT=%v", utils.Config.Auth.Sessions.InactivityTimeout))
}
for id, tmpl := range utils.Config.Auth.Email.Template {
if len(tmpl.ContentPath) > 0 {
env = append(env, fmt.Sprintf("GOTRUE_MAILER_TEMPLATES_%s=http://%s:%d/email/%s",
strings.ToUpper(id),
utils.KongId,
nginxTemplateServerPort,
id+filepath.Ext(tmpl.ContentPath),
))
}
if tmpl.Subject != nil {
env = append(env, fmt.Sprintf("GOTRUE_MAILER_SUBJECTS_%s=%s",
strings.ToUpper(id),
*tmpl.Subject,
))
}
}
switch {
case utils.Config.Auth.Sms.Twilio.Enabled:
env = append(
env,
"GOTRUE_SMS_PROVIDER=twilio",
"GOTRUE_SMS_TWILIO_ACCOUNT_SID="+utils.Config.Auth.Sms.Twilio.AccountSid,
"GOTRUE_SMS_TWILIO_AUTH_TOKEN="+utils.Config.Auth.Sms.Twilio.AuthToken.Value,
"GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID="+utils.Config.Auth.Sms.Twilio.MessageServiceSid,
)
case utils.Config.Auth.Sms.TwilioVerify.Enabled:
env = append(
env,
"GOTRUE_SMS_PROVIDER=twilio_verify",
"GOTRUE_SMS_TWILIO_VERIFY_ACCOUNT_SID="+utils.Config.Auth.Sms.TwilioVerify.AccountSid,
"GOTRUE_SMS_TWILIO_VERIFY_AUTH_TOKEN="+utils.Config.Auth.Sms.TwilioVerify.AuthToken.Value,
"GOTRUE_SMS_TWILIO_VERIFY_MESSAGE_SERVICE_SID="+utils.Config.Auth.Sms.TwilioVerify.MessageServiceSid,
)
case utils.Config.Auth.Sms.Messagebird.Enabled:
env = append(
env,
"GOTRUE_SMS_PROVIDER=messagebird",
"GOTRUE_SMS_MESSAGEBIRD_ACCESS_KEY="+utils.Config.Auth.Sms.Messagebird.AccessKey.Value,
"GOTRUE_SMS_MESSAGEBIRD_ORIGINATOR="+utils.Config.Auth.Sms.Messagebird.Originator,
)
case utils.Config.Auth.Sms.Textlocal.Enabled:
env = append(
env,
"GOTRUE_SMS_PROVIDER=textlocal",
"GOTRUE_SMS_TEXTLOCAL_API_KEY="+utils.Config.Auth.Sms.Textlocal.ApiKey.Value,
"GOTRUE_SMS_TEXTLOCAL_SENDER="+utils.Config.Auth.Sms.Textlocal.Sender,
)
case utils.Config.Auth.Sms.Vonage.Enabled:
env = append(
env,
"GOTRUE_SMS_PROVIDER=vonage",
"GOTRUE_SMS_VONAGE_API_KEY="+utils.Config.Auth.Sms.Vonage.ApiKey,
"GOTRUE_SMS_VONAGE_API_SECRET="+utils.Config.Auth.Sms.Vonage.ApiSecret.Value,
"GOTRUE_SMS_VONAGE_FROM="+utils.Config.Auth.Sms.Vonage.From,
)
}
if captcha := utils.Config.Auth.Captcha; captcha != nil {
env = append(
env,
fmt.Sprintf("GOTRUE_SECURITY_CAPTCHA_ENABLED=%v", captcha.Enabled),
fmt.Sprintf("GOTRUE_SECURITY_CAPTCHA_PROVIDER=%v", captcha.Provider),
fmt.Sprintf("GOTRUE_SECURITY_CAPTCHA_SECRET=%v", captcha.Secret.Value),
)
}
if hook := utils.Config.Auth.Hook.MFAVerificationAttempt; hook != nil && hook.Enabled {
env = append(
env,
"GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED=true",
"GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI="+hook.URI,
"GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_SECRETS="+hook.Secrets.Value,
)
}
if hook := utils.Config.Auth.Hook.PasswordVerificationAttempt; hook != nil && hook.Enabled {
env = append(
env,
"GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED=true",
"GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI="+hook.URI,
"GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_SECRETS="+hook.Secrets.Value,
)
}
if hook := utils.Config.Auth.Hook.CustomAccessToken; hook != nil && hook.Enabled {
env = append(
env,
"GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED=true",
"GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI="+hook.URI,
"GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS="+hook.Secrets.Value,
)
}
if hook := utils.Config.Auth.Hook.SendSMS; hook != nil && hook.Enabled {
env = append(
env,
"GOTRUE_HOOK_SEND_SMS_ENABLED=true",
"GOTRUE_HOOK_SEND_SMS_URI="+hook.URI,
"GOTRUE_HOOK_SEND_SMS_SECRETS="+hook.Secrets.Value,
)
}
if hook := utils.Config.Auth.Hook.SendEmail; hook != nil && hook.Enabled {
env = append(
env,
"GOTRUE_HOOK_SEND_EMAIL_ENABLED=true",
"GOTRUE_HOOK_SEND_EMAIL_URI="+hook.URI,
"GOTRUE_HOOK_SEND_EMAIL_SECRETS="+hook.Secrets.Value,
)
}
if utils.Config.Auth.MFA.Phone.EnrollEnabled || utils.Config.Auth.MFA.Phone.VerifyEnabled {
env = append(
env,
"GOTRUE_MFA_PHONE_TEMPLATE="+utils.Config.Auth.MFA.Phone.Template,
fmt.Sprintf("GOTRUE_MFA_PHONE_OTP_LENGTH=%v", utils.Config.Auth.MFA.Phone.OtpLength),
fmt.Sprintf("GOTRUE_MFA_PHONE_MAX_FREQUENCY=%v", utils.Config.Auth.MFA.Phone.MaxFrequency),
)
}
for name, config := range utils.Config.Auth.External {
env = append(
env,
fmt.Sprintf("GOTRUE_EXTERNAL_%s_ENABLED=%v", strings.ToUpper(name), config.Enabled),
fmt.Sprintf("GOTRUE_EXTERNAL_%s_CLIENT_ID=%s", strings.ToUpper(name), config.ClientId),
fmt.Sprintf("GOTRUE_EXTERNAL_%s_SECRET=%s", strings.ToUpper(name), config.Secret.Value),
fmt.Sprintf("GOTRUE_EXTERNAL_%s_SKIP_NONCE_CHECK=%t", strings.ToUpper(name), config.SkipNonceCheck),
)
redirectUri := config.RedirectUri
if redirectUri == "" {
redirectUri = utils.GetApiUrl("/auth/v1/callback")
}
env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_REDIRECT_URI=%s", strings.ToUpper(name), redirectUri))
if config.Url != "" {
env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_URL=%s", strings.ToUpper(name), config.Url))
}
}
if _, err := utils.DockerStart(
ctx,
container.Config{
Image: utils.Config.Auth.Image,
Env: env,
ExposedPorts: nat.PortSet{"9999/tcp": {}},
Healthcheck: &container.HealthConfig{
Test: []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://127.0.0.1:9999/health",
},
Interval: 10 * time.Second,
Timeout: 2 * time.Second,
Retries: 3,
},
},
container.HostConfig{
RestartPolicy: container.RestartPolicy{Name: "always"},
},
network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
utils.NetId: {
Aliases: utils.GotrueAliases,
},
},
},
utils.GotrueId,
); err != nil {
return err
}
started = append(started, utils.GotrueId)
}
// Start PostgREST.
if utils.Config.Api.Enabled && !isContainerExcluded(utils.Config.Api.Image, excluded) {
if _, err := utils.DockerStart(
ctx,
container.Config{
Image: utils.Config.Api.Image,
Env: []string{
fmt.Sprintf("PGRST_DB_URI=postgresql://authenticator:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
"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),
"PGRST_DB_ANON_ROLE=anon",
fmt.Sprintf("PGRST_JWT_SECRET=%s", jwks),
"PGRST_ADMIN_SERVER_PORT=3001",
},
// PostgREST does not expose a shell for health check
},
container.HostConfig{
RestartPolicy: container.RestartPolicy{Name: "always"},
},
network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
utils.NetId: {
Aliases: utils.RestAliases,
},
},
},
utils.RestId,
); err != nil {
return err
}
started = append(started, utils.RestId)
}
// Start Storage.
if isStorageEnabled {
dockerStoragePath := "/mnt"
if _, err := utils.DockerStart(
ctx,
container.Config{
Image: utils.Config.Storage.Image,
Env: []string{
"ANON_KEY=" + utils.Config.Auth.AnonKey,
"SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey,
"AUTH_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
fmt.Sprintf("AUTH_JWT_JWKS=%s", jwks),
fmt.Sprintf("DATABASE_URL=postgresql://supabase_storage_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit),
"STORAGE_BACKEND=file",
"FILE_STORAGE_BACKEND_PATH=" + dockerStoragePath,
"TENANT_ID=stub",
// TODO: https://github.com/supabase/storage-api/issues/55
"STORAGE_S3_REGION=" + utils.Config.Storage.S3Credentials.Region,
"GLOBAL_S3_BUCKET=stub",
fmt.Sprintf("ENABLE_IMAGE_TRANSFORMATION=%t", isImgProxyEnabled),
fmt.Sprintf("IMGPROXY_URL=http://%s:5001", utils.ImgProxyId),
"TUS_URL_PATH=/storage/v1/upload/resumable",
"S3_PROTOCOL_ACCESS_KEY_ID=" + utils.Config.Storage.S3Credentials.AccessKeyId,
"S3_PROTOCOL_ACCESS_KEY_SECRET=" + utils.Config.Storage.S3Credentials.SecretAccessKey,
"S3_PROTOCOL_PREFIX=/storage/v1",
fmt.Sprintf("S3_ALLOW_FORWARDED_HEADER=%v", StorageVersionBelow("1.10.1")),
"UPLOAD_FILE_SIZE_LIMIT=52428800000",
"UPLOAD_FILE_SIZE_LIMIT_STANDARD=5242880000",
},
Healthcheck: &container.HealthConfig{
// For some reason, localhost resolves to IPv6 address on GitPod which breaks healthcheck.
Test: []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://127.0.0.1:5000/status",
},
Interval: 10 * time.Second,
Timeout: 2 * time.Second,
Retries: 3,
},
},
container.HostConfig{
RestartPolicy: container.RestartPolicy{Name: "always"},
Binds: []string{utils.StorageId + ":" + dockerStoragePath},
},
network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
utils.NetId: {
Aliases: utils.StorageAliases,
},
},
},
utils.StorageId,
); err != nil {
return err
}
started = append(started, utils.StorageId)
}
p.Send(utils.StatusMsg("Waiting for health checks..."))
if utils.NoBackupVolume && utils.SliceContains(started, utils.StorageId) {
if err := start.WaitForHealthyService(ctx, serviceTimeout, utils.StorageId); err != nil {
return err
}
// Disable prompts when seeding
if err := buckets.Run(ctx, "", false, fsys); err != nil {
return err
}
}
return start.WaitForHealthyService(ctx, serviceTimeout, started...)
}
func isContainerExcluded(imageName string, excluded map[string]bool) bool {
short := utils.ShortContainerImageName(imageName)
val, ok := excluded[short]
return ok && val
}
func ExcludableContainers() []string {
names := []string{}
for _, image := range config.Images.Services() {
names = append(names, utils.ShortContainerImageName(image))
}
return names
}
func formatMapForEnvConfig(input map[string]string, output *bytes.Buffer) {
numOfKeyPairs := len(input)
i := 0
for k, v := range input {
output.WriteString(k)
output.WriteString(":")
output.WriteString(v)
i++
if i < numOfKeyPairs {
output.WriteString(",")
}
}
}