github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/start/start.go (about) 1 package start 2 3 import ( 4 "bytes" 5 "context" 6 _ "embed" 7 "errors" 8 "fmt" 9 "os" 10 "strconv" 11 "strings" 12 "text/template" 13 "time" 14 15 "github.com/Redstoneguy129/cli/internal/db/reset" 16 "github.com/Redstoneguy129/cli/internal/db/start" 17 "github.com/Redstoneguy129/cli/internal/status" 18 "github.com/Redstoneguy129/cli/internal/utils" 19 "github.com/docker/docker/api/types/container" 20 "github.com/docker/go-connections/nat" 21 "github.com/jackc/pgx/v4" 22 "github.com/spf13/afero" 23 ) 24 25 var errUnhealthy = errors.New("service not healthy") 26 27 func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignoreHealthCheck bool) error { 28 // Sanity checks. 29 { 30 if err := utils.LoadConfigFS(fsys); err != nil { 31 return err 32 } 33 if err := utils.AssertDockerIsRunning(ctx); err != nil { 34 return err 35 } 36 if _, err := utils.Docker.ContainerInspect(ctx, utils.DbId); err == nil { 37 fmt.Fprintln(os.Stderr, utils.Aqua("supabase start")+" is already running.") 38 return nil 39 } 40 } 41 42 if err := utils.RunProgram(ctx, func(p utils.Program, ctx context.Context) error { 43 return run(p, ctx, fsys, excludedContainers) 44 }); err != nil { 45 if ignoreHealthCheck && errors.Is(err, errUnhealthy) { 46 fmt.Fprintln(os.Stderr, err) 47 } else { 48 utils.DockerRemoveAll(context.Background()) 49 return err 50 } 51 } 52 53 fmt.Fprintf(os.Stderr, "Started %s local development setup.\n\n", utils.Aqua("supabase")) 54 status.PrettyPrint(os.Stdout, excludedContainers...) 55 return nil 56 } 57 58 var ( 59 // TODO: Unhardcode keys 60 //go:embed templates/kong_config 61 kongConfigEmbed string 62 kongConfigTemplate = template.Must(template.New("kongConfig").Parse(kongConfigEmbed)) 63 ) 64 65 func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers []string, options ...func(*pgx.ConnConfig)) error { 66 excluded := make(map[string]bool) 67 for _, name := range excludedContainers { 68 excluded[name] = true 69 } 70 71 // Pull images. 72 { 73 total := len(utils.ServiceImages) + 1 74 p.Send(utils.StatusMsg(fmt.Sprintf("Pulling images... (1/%d)", total))) 75 if err := utils.DockerPullImageIfNotCached(ctx, utils.DbImage); err != nil { 76 return err 77 } 78 for i, image := range utils.ServiceImages { 79 if isContainerExcluded(image, excluded) { 80 fmt.Fprintln(os.Stderr, "Excluding container:", image) 81 continue 82 } 83 p.Send(utils.StatusMsg(fmt.Sprintf("Pulling images... (%d/%d)", i+1, total))) 84 if err := utils.DockerPullImageIfNotCached(ctx, image); err != nil { 85 return err 86 } 87 } 88 } 89 90 // Start Postgres. 91 w := utils.StatusWriter{Program: p} 92 if err := start.StartDatabase(ctx, fsys, w, options...); err != nil { 93 return err 94 } 95 96 p.Send(utils.StatusMsg("Starting containers...")) 97 var started []string 98 99 // Start Kong. 100 if !isContainerExcluded(utils.KongImage, excluded) { 101 var kongConfigBuf bytes.Buffer 102 if err := kongConfigTemplate.Execute(&kongConfigBuf, struct{ ProjectId, AnonKey, ServiceRoleKey string }{ 103 ProjectId: utils.Config.ProjectId, 104 AnonKey: utils.AnonKey, 105 ServiceRoleKey: utils.ServiceRoleKey, 106 }); err != nil { 107 return err 108 } 109 110 if _, err := utils.DockerStart( 111 ctx, 112 container.Config{ 113 Image: utils.KongImage, 114 Env: []string{ 115 "KONG_DATABASE=off", 116 "KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml", 117 "KONG_DNS_ORDER=LAST,A,CNAME", // https://github.com/Redstoneguy129/cli/issues/14 118 "KONG_PLUGINS=request-transformer,cors,key-auth", 119 // Need to increase the nginx buffers in kong to avoid it rejecting the rather 120 // sizeable response headers azure can generate 121 // Ref: https://github.com/Kong/kong/issues/3974#issuecomment-482105126 122 "KONG_NGINX_PROXY_PROXY_BUFFER_SIZE=160k", 123 "KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k", 124 }, 125 Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /home/kong/kong.yml && ./docker-entrypoint.sh kong docker-start 126 ` + kongConfigBuf.String() + ` 127 EOF 128 `}, 129 }, 130 container.HostConfig{ 131 PortBindings: nat.PortMap{"8000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Api.Port), 10)}}}, 132 RestartPolicy: container.RestartPolicy{Name: "always"}, 133 }, 134 utils.KongId, 135 ); err != nil { 136 return err 137 } 138 started = append(started, utils.KongId) 139 } 140 141 // Start GoTrue. 142 if !isContainerExcluded(utils.GotrueImage, excluded) { 143 env := []string{ 144 fmt.Sprintf("API_EXTERNAL_URL=http://"+utils.Config.Hostname+":%v", utils.Config.Api.Port), 145 146 "GOTRUE_API_HOST=0.0.0.0", 147 "GOTRUE_API_PORT=9999", 148 149 "GOTRUE_DB_DRIVER=postgres", 150 "GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:postgres@" + utils.DbId + ":5432/postgres", 151 152 "GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl, 153 "GOTRUE_URI_ALLOW_LIST=" + strings.Join(utils.Config.Auth.AdditionalRedirectUrls, ","), 154 fmt.Sprintf("GOTRUE_DISABLE_SIGNUP=%v", !*utils.Config.Auth.EnableSignup), 155 156 "GOTRUE_JWT_ADMIN_ROLES=service_role", 157 "GOTRUE_JWT_AUD=authenticated", 158 "GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated", 159 fmt.Sprintf("GOTRUE_JWT_EXP=%v", utils.Config.Auth.JwtExpiry), 160 "GOTRUE_JWT_SECRET=" + utils.JWTSecret, 161 162 fmt.Sprintf("GOTRUE_EXTERNAL_EMAIL_ENABLED=%v", *utils.Config.Auth.Email.EnableSignup), 163 fmt.Sprintf("GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=%v", *utils.Config.Auth.Email.DoubleConfirmChanges), 164 fmt.Sprintf("GOTRUE_MAILER_AUTOCONFIRM=%v", !*utils.Config.Auth.Email.EnableConfirmations), 165 166 "GOTRUE_SMTP_HOST=" + utils.InbucketId, 167 "GOTRUE_SMTP_PORT=2500", 168 "GOTRUE_SMTP_ADMIN_EMAIL=admin@email.com", 169 "GOTRUE_SMTP_MAX_FREQUENCY=1s", 170 "GOTRUE_MAILER_URLPATHS_INVITE=/auth/v1/verify", 171 "GOTRUE_MAILER_URLPATHS_CONFIRMATION=/auth/v1/verify", 172 "GOTRUE_MAILER_URLPATHS_RECOVERY=/auth/v1/verify", 173 "GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=/auth/v1/verify", 174 "GOTRUE_RATE_LIMIT_EMAIL_SENT=360000", 175 176 "GOTRUE_EXTERNAL_PHONE_ENABLED=true", 177 "GOTRUE_SMS_AUTOCONFIRM=true", 178 179 "GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=false", 180 } 181 182 for name, config := range utils.Config.Auth.External { 183 env = append( 184 env, 185 fmt.Sprintf("GOTRUE_EXTERNAL_%s_ENABLED=%v", strings.ToUpper(name), config.Enabled), 186 fmt.Sprintf("GOTRUE_EXTERNAL_%s_CLIENT_ID=%s", strings.ToUpper(name), config.ClientId), 187 fmt.Sprintf("GOTRUE_EXTERNAL_%s_SECRET=%s", strings.ToUpper(name), config.Secret), 188 ) 189 190 if config.RedirectUri != "" { 191 env = append(env, 192 fmt.Sprintf("GOTRUE_EXTERNAL_%s_REDIRECT_URI=%s", strings.ToUpper(name), config.RedirectUri), 193 ) 194 } else { 195 env = append(env, 196 fmt.Sprintf("GOTRUE_EXTERNAL_%s_REDIRECT_URI=http://"+utils.Config.Hostname+":%v/auth/v1/callback", strings.ToUpper(name), utils.Config.Api.Port), 197 ) 198 } 199 200 if config.Url != "" { 201 env = append(env, 202 fmt.Sprintf("GOTRUE_EXTERNAL_%s_URL=%s", strings.ToUpper(name), config.Url), 203 ) 204 } 205 } 206 207 if _, err := utils.DockerStart( 208 ctx, 209 container.Config{ 210 Image: utils.GotrueImage, 211 Env: env, 212 Healthcheck: &container.HealthConfig{ 213 Test: []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://\"+utils.Config.Hostname+\":9999/health"}, 214 Interval: 2 * time.Second, 215 Timeout: 2 * time.Second, 216 Retries: 10, 217 }, 218 }, 219 container.HostConfig{ 220 RestartPolicy: container.RestartPolicy{Name: "always"}, 221 }, 222 utils.GotrueId, 223 ); err != nil { 224 return err 225 } 226 started = append(started, utils.GotrueId) 227 } 228 229 // Start Inbucket. 230 if !isContainerExcluded(utils.InbucketImage, excluded) { 231 inbucketPortBindings := nat.PortMap{"9000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.Port), 10)}}} 232 if utils.Config.Inbucket.SmtpPort != 0 { 233 inbucketPortBindings["2500/tcp"] = []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.SmtpPort), 10)}} 234 } 235 if utils.Config.Inbucket.Pop3Port != 0 { 236 inbucketPortBindings["1100/tcp"] = []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.Pop3Port), 10)}} 237 } 238 if _, err := utils.DockerStart( 239 ctx, 240 container.Config{ 241 Image: utils.InbucketImage, 242 }, 243 container.HostConfig{ 244 PortBindings: inbucketPortBindings, 245 RestartPolicy: container.RestartPolicy{Name: "always"}, 246 }, 247 utils.InbucketId, 248 ); err != nil { 249 return err 250 } 251 started = append(started, utils.InbucketId) 252 } 253 254 // Start Realtime. 255 if !isContainerExcluded(utils.RealtimeImage, excluded) { 256 if _, err := utils.DockerStart( 257 ctx, 258 container.Config{ 259 Image: utils.RealtimeImage, 260 Env: []string{ 261 "PORT=4000", 262 "DB_HOST=" + utils.DbId, 263 "DB_PORT=5432", 264 "DB_USER=postgres", 265 "DB_PASSWORD=postgres", 266 "DB_NAME=postgres", 267 "DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime", 268 "DB_ENC_KEY=supabaserealtime", 269 "API_JWT_SECRET=" + utils.JWTSecret, 270 "FLY_ALLOC_ID=abc123", 271 "FLY_APP_NAME=realtime", 272 "SECRET_KEY_BASE=EAx3IQ/wRG1v47ZD4NE4/9RzBI8Jmil3x0yhcW4V2NHBP6c2iPIzwjofi2Ep4HIG", 273 "ERL_AFLAGS=-proto_dist inet_tcp", 274 "ENABLE_TAILSCALE=false", 275 "DNS_NODES=''", 276 }, 277 Cmd: []string{ 278 "/bin/sh", "-c", 279 "/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server", 280 }, 281 Healthcheck: &container.HealthConfig{ 282 Test: []string{"CMD", "bash", "-c", "printf \\0 > /dev/tcp/\"+utils.Config.Hostname+\"/4000"}, 283 Interval: 2 * time.Second, 284 Timeout: 2 * time.Second, 285 Retries: 10, 286 }, 287 }, 288 container.HostConfig{ 289 RestartPolicy: container.RestartPolicy{Name: "always"}, 290 }, 291 utils.RealtimeId, 292 ); err != nil { 293 return err 294 } 295 started = append(started, utils.RealtimeId) 296 } 297 298 // Start PostgREST. 299 if !isContainerExcluded(utils.PostgrestImage, excluded) { 300 if _, err := utils.DockerStart( 301 ctx, 302 container.Config{ 303 Image: utils.PostgrestImage, 304 Env: []string{ 305 "PGRST_DB_URI=postgresql://authenticator:postgres@" + utils.DbId + ":5432/postgres", 306 "PGRST_DB_SCHEMAS=" + strings.Join(utils.Config.Api.Schemas, ","), 307 "PGRST_DB_EXTRA_SEARCH_PATH=" + strings.Join(utils.Config.Api.ExtraSearchPath, ","), 308 "PGRST_DB_ANON_ROLE=anon", 309 "PGRST_JWT_SECRET=" + utils.JWTSecret, 310 }, 311 // PostgREST does not expose a shell for health check 312 }, 313 container.HostConfig{ 314 RestartPolicy: container.RestartPolicy{Name: "always"}, 315 }, 316 utils.RestId, 317 ); err != nil { 318 return err 319 } 320 started = append(started, utils.RestId) 321 } 322 323 // Start Storage. 324 if !isContainerExcluded(utils.StorageImage, excluded) { 325 if _, err := utils.DockerStart( 326 ctx, 327 container.Config{ 328 Image: utils.StorageImage, 329 Env: []string{ 330 "ANON_KEY=" + utils.AnonKey, 331 "SERVICE_KEY=" + utils.ServiceRoleKey, 332 "POSTGREST_URL=http://" + utils.RestId + ":3000", 333 "PGRST_JWT_SECRET=" + utils.JWTSecret, 334 "DATABASE_URL=postgresql://supabase_storage_admin:postgres@" + utils.DbId + ":5432/postgres", 335 fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit), 336 "STORAGE_BACKEND=file", 337 "FILE_STORAGE_BACKEND_PATH=/var/lib/storage", 338 "TENANT_ID=stub", 339 // TODO: https://github.com/supabase/storage-api/issues/55 340 "REGION=stub", 341 "GLOBAL_S3_BUCKET=stub", 342 "ENABLE_IMAGE_TRANSFORMATION=true", 343 "IMGPROXY_URL=http://" + utils.ImgProxyId + ":5001", 344 }, 345 Healthcheck: &container.HealthConfig{ 346 Test: []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://" + utils.Config.Hostname + ":5000/status"}, 347 Interval: 2 * time.Second, 348 Timeout: 2 * time.Second, 349 Retries: 10, 350 }, 351 }, 352 container.HostConfig{ 353 RestartPolicy: container.RestartPolicy{Name: "always"}, 354 Binds: []string{utils.StorageId + ":/var/lib/storage"}, 355 }, 356 utils.StorageId, 357 ); err != nil { 358 return err 359 } 360 started = append(started, utils.StorageId) 361 } 362 363 // Start Storage ImgProxy. 364 if !isContainerExcluded(utils.ImageProxyImage, excluded) { 365 if _, err := utils.DockerStart( 366 ctx, 367 container.Config{ 368 Image: utils.ImageProxyImage, 369 Env: []string{ 370 "IMGPROXY_BIND=:5001", 371 "IMGPROXY_LOCAL_FILESYSTEM_ROOT=/", 372 "IMGPROXY_USE_ETAG=/", 373 }, 374 Healthcheck: &container.HealthConfig{ 375 Test: []string{"CMD", "imgproxy", "health"}, 376 Interval: 2 * time.Second, 377 Timeout: 2 * time.Second, 378 Retries: 10, 379 }, 380 }, 381 container.HostConfig{ 382 VolumesFrom: []string{utils.StorageId}, 383 RestartPolicy: container.RestartPolicy{Name: "always"}, 384 }, 385 utils.ImgProxyId, 386 ); err != nil { 387 return err 388 } 389 started = append(started, utils.ImgProxyId) 390 } 391 392 // Start pg-meta. 393 if !isContainerExcluded(utils.PgmetaImage, excluded) { 394 if _, err := utils.DockerStart( 395 ctx, 396 container.Config{ 397 Image: utils.PgmetaImage, 398 Env: []string{ 399 "PG_META_PORT=8080", 400 "PG_META_DB_HOST=" + utils.DbId, 401 }, 402 Healthcheck: &container.HealthConfig{ 403 Test: []string{"CMD", "node", "-e", "require('http').get('http://'+utils.Config.Hostname+':8080/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"}, 404 Interval: 2 * time.Second, 405 Timeout: 2 * time.Second, 406 Retries: 10, 407 }, 408 }, 409 container.HostConfig{ 410 RestartPolicy: container.RestartPolicy{Name: "always"}, 411 }, 412 utils.PgmetaId, 413 ); err != nil { 414 return err 415 } 416 started = append(started, utils.PgmetaId) 417 } 418 419 // Start Studio. 420 if !isContainerExcluded(utils.StudioImage, excluded) { 421 if _, err := utils.DockerStart( 422 ctx, 423 container.Config{ 424 Image: utils.StudioImage, 425 Env: []string{ 426 "STUDIO_PG_META_URL=http://" + utils.PgmetaId + ":8080", 427 "POSTGRES_PASSWORD=postgres", 428 "SUPABASE_URL=http://" + utils.KongId + ":8000", 429 fmt.Sprintf("SUPABASE_REST_URL=http://"+utils.Config.Hostname+":%v/rest/v1/", utils.Config.Api.Port), 430 fmt.Sprintf("SUPABASE_PUBLIC_URL=http://"+utils.Config.Hostname+":%v/", utils.Config.Api.Port), 431 "SUPABASE_ANON_KEY=" + utils.AnonKey, 432 "SUPABASE_SERVICE_KEY=" + utils.ServiceRoleKey, 433 }, 434 Healthcheck: &container.HealthConfig{ 435 Test: []string{"CMD", "node", "-e", "require('http').get('http:/'+utils.Config.Hostname+':3000/api/profile', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"}, 436 Interval: 2 * time.Second, 437 Timeout: 2 * time.Second, 438 Retries: 10, 439 }, 440 }, 441 container.HostConfig{ 442 PortBindings: nat.PortMap{"3000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Studio.Port), 10)}}}, 443 RestartPolicy: container.RestartPolicy{Name: "always"}, 444 }, 445 utils.StudioId, 446 ); err != nil { 447 return err 448 } 449 started = append(started, utils.StudioId) 450 } 451 452 return waitForServiceReady(ctx, started) 453 } 454 455 func isContainerExcluded(imageName string, excluded map[string]bool) bool { 456 short := utils.ShortContainerImageName(imageName) 457 if val, ok := excluded[short]; ok && val { 458 return true 459 } 460 return false 461 } 462 463 func ExcludableContainers() []string { 464 names := []string{} 465 for _, image := range utils.ServiceImages { 466 names = append(names, utils.ShortContainerImageName(image)) 467 } 468 return names 469 } 470 471 func waitForServiceReady(ctx context.Context, started []string) error { 472 probe := func() bool { 473 var unhealthy []string 474 for _, container := range started { 475 if !status.IsServiceReady(ctx, container) { 476 unhealthy = append(unhealthy, container) 477 } 478 } 479 started = unhealthy 480 return len(started) == 0 481 } 482 if !reset.RetryEverySecond(ctx, probe, 20*time.Second) { 483 return fmt.Errorf("%w: %v", errUnhealthy, started) 484 } 485 return nil 486 }