664 lines
23 KiB
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(",")
|
|
}
|
|
}
|
|
}
|