Skip to content

Commit b8534fb

Browse files
committed
feat(apple-containers): add Apple Containers runtime support
Allow Supabase CLI-managed services to run on Apple Containers via --runtime apple-container or [local].runtime in config.toml. Keep Docker as the default runtime while adding runtime-aware service lifecycle handling, analytics log forwarding, status reporting, and related test coverage for Apple container execution paths.
1 parent e602282 commit b8534fb

34 files changed

+3738
-632
lines changed

cmd/apple_log_forwarder.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package cmd
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/supabase/cli/internal/utils"
6+
)
7+
8+
var (
9+
appleLogForwarderContainer string
10+
appleLogForwarderOutput string
11+
12+
appleLogForwarderCmd = &cobra.Command{
13+
Use: "apple-log-forwarder",
14+
Short: "Internal Apple analytics log forwarder",
15+
Hidden: true,
16+
RunE: func(cmd *cobra.Command, args []string) error {
17+
return utils.RunAppleAnalyticsLogForwarder(cmd.Context(), appleLogForwarderContainer, appleLogForwarderOutput)
18+
},
19+
}
20+
)
21+
22+
func init() {
23+
flags := appleLogForwarderCmd.Flags()
24+
flags.StringVar(&appleLogForwarderContainer, "container", "", "container id to follow")
25+
flags.StringVar(&appleLogForwarderOutput, "output", "", "output path for JSONL logs")
26+
cobra.CheckErr(appleLogForwarderCmd.MarkFlagRequired("container"))
27+
cobra.CheckErr(appleLogForwarderCmd.MarkFlagRequired("output"))
28+
rootCmd.AddCommand(appleLogForwarderCmd)
29+
}

cmd/root.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010
"os/signal"
1111
"strings"
12+
"syscall"
1213
"time"
1314

1415
"github.com/getsentry/sentry-go"
@@ -94,7 +95,7 @@ var (
9495
}
9596
cmd.SilenceUsage = true
9697
// Load profile before changing workdir
97-
ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
98+
ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
9899
fsys := afero.NewOsFs()
99100
if err := utils.LoadProfile(ctx, fsys); err != nil {
100101
return err
@@ -203,7 +204,7 @@ func recoverAndExit() {
203204
!viper.GetBool("DEBUG") {
204205
utils.CmdSuggestion = utils.SuggestDebugFlag
205206
}
206-
if e, ok := err.(*errors.Error); ok && len(utils.Version) == 0 {
207+
if e, ok := err.(*errors.Error); ok && viper.GetBool("DEBUG") {
207208
fmt.Fprintln(os.Stderr, string(e.Stack()))
208209
}
209210
msg = err.Error()
@@ -240,6 +241,7 @@ func init() {
240241
flags.String("workdir", "", "path to a Supabase project directory")
241242
flags.Bool("experimental", false, "enable experimental features")
242243
flags.String("network-id", "", "use the specified docker network instead of a generated one")
244+
flags.String("runtime", "", "container runtime for local development (docker|apple-container)")
243245
flags.String("profile", "supabase", "use a specific profile for connecting to Supabase API")
244246
flags.VarP(&utils.OutputFormat, "output", "o", "output format of status variables")
245247
flags.Var(&utils.DNSResolver, "dns-resolver", "lookup domain names using the specified resolver")

docs/supabase/start.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Starts the Supabase local development stack.
44

55
Requires `supabase/config.toml` to be created in your current working directory by running `supabase init`.
66

7+
Use `--runtime` to override the local container runtime for the current command. To make it persistent for the project, set `[runtime].backend` in `supabase/config.toml`.
8+
79
All service containers are started by default. You can exclude those not needed by passing in `-x` flag. To exclude multiple containers, either pass in a comma separated string, such as `-x gotrue,imgproxy`, or specify `-x` flag multiple times.
810

911
> It is recommended to have at least 7GB of RAM to start all services.

docs/supabase/status.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ Shows status of the Supabase local development stack.
44

55
Requires the local development stack to be started by running `supabase start` or `supabase db start`.
66

7+
The pretty output includes a runtime summary with the selected local runtime, project id, and tracked containers, networks, and volumes.
8+
79
You can export the connection parameters for [initializing supabase-js](https://supabase.com/docs/reference/javascript/initializing) locally by specifying the `-o env` flag. Supported parameters include `JWT_SECRET`, `ANON_KEY`, and `SERVICE_ROLE_KEY`.

docs/supabase/stop.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ Stops the Supabase local development stack.
44

55
Requires `supabase/config.toml` to be created in your current working directory by running `supabase init`.
66

7-
All Docker resources are maintained across restarts. Use `--no-backup` flag to reset your local development data between restarts.
7+
Local container resources are maintained across restarts for both the `docker` and `apple-container` runtimes. Use `--no-backup` flag to reset your local development data between restarts.
88

9-
Use the `--all` flag to stop all local Supabase projects instances on the machine. Use with caution with `--no-backup` as it will delete all supabase local projects data.
9+
Use the `--all` flag to stop all local Supabase projects instances on the machine. Use with caution with `--no-backup` as it will delete all supabase local projects data.

internal/db/reset/reset.go

Lines changed: 98 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,47 @@ import (
55
_ "embed"
66
"fmt"
77
"io"
8+
"net"
89
"os"
910
"strconv"
1011
"strings"
1112
"time"
1213

1314
"github.com/cenkalti/backoff/v4"
1415
"github.com/containerd/errdefs"
15-
"github.com/docker/docker/api/types"
16-
"github.com/docker/docker/api/types/container"
1716
"github.com/docker/docker/api/types/network"
1817
"github.com/go-errors/errors"
1918
"github.com/jackc/pgconn"
2019
"github.com/jackc/pgerrcode"
2120
"github.com/jackc/pgx/v4"
2221
"github.com/spf13/afero"
23-
"github.com/supabase/cli/internal/db/start"
22+
dbstart "github.com/supabase/cli/internal/db/start"
2423
"github.com/supabase/cli/internal/migration/apply"
2524
"github.com/supabase/cli/internal/migration/down"
2625
"github.com/supabase/cli/internal/migration/list"
2726
"github.com/supabase/cli/internal/migration/repair"
2827
"github.com/supabase/cli/internal/seed/buckets"
28+
stackstart "github.com/supabase/cli/internal/start"
2929
"github.com/supabase/cli/internal/utils"
3030
"github.com/supabase/cli/pkg/migration"
3131
)
3232

33+
var (
34+
assertSupabaseDbIsRunning = utils.AssertSupabaseDbIsRunning
35+
removeContainer = utils.RemoveContainer
36+
removeVolume = utils.RemoveVolume
37+
startContainer = utils.DockerStart
38+
inspectContainer = utils.InspectContainer
39+
restartContainer = utils.RestartContainer
40+
waitForHealthyService = dbstart.WaitForHealthyService
41+
waitForLocalDatabase = waitForDatabaseReady
42+
waitForLocalAPI = waitForAPIReady
43+
setupLocalDatabase = dbstart.SetupLocalDatabase
44+
restartKong = stackstart.RestartKong
45+
runBucketSeed = buckets.Run
46+
seedBuckets = seedBucketsWithRetry
47+
)
48+
3349
func Run(ctx context.Context, version string, last uint, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
3450
if len(version) > 0 {
3551
if _, err := strconv.Atoi(version); err != nil {
@@ -54,21 +70,38 @@ func Run(ctx context.Context, version string, last uint, config pgconn.Config, f
5470
return resetRemote(ctx, version, config, fsys, options...)
5571
}
5672
// Config file is loaded before parsing --linked or --local flags
57-
if err := utils.AssertSupabaseDbIsRunning(); err != nil {
73+
if err := assertSupabaseDbIsRunning(); err != nil {
5874
return err
5975
}
6076
// Reset postgres database because extensions (pg_cron, pg_net) require postgres
6177
if err := resetDatabase(ctx, version, fsys, options...); err != nil {
6278
return err
6379
}
6480
// Seed objects from supabase/buckets directory
65-
if resp, err := utils.Docker.ContainerInspect(ctx, utils.StorageId); err == nil {
66-
if resp.State.Health == nil || resp.State.Health.Status != types.Healthy {
67-
if err := start.WaitForHealthyService(ctx, 30*time.Second, utils.StorageId); err != nil {
81+
if _, err := inspectContainer(ctx, utils.StorageId); err == nil {
82+
if shouldRefreshAPIAfterReset() {
83+
// Kong caches upstream addresses; recreate it after the db container gets a new IP.
84+
if err := restartKong(ctx, stackstart.KongDependencies{
85+
Gotrue: utils.Config.Auth.Enabled,
86+
Rest: utils.Config.Api.Enabled,
87+
Realtime: utils.Config.Realtime.Enabled,
88+
Storage: utils.Config.Storage.Enabled,
89+
Studio: utils.Config.Studio.Enabled,
90+
Pgmeta: utils.Config.Studio.Enabled,
91+
Edge: true,
92+
Logflare: utils.Config.Analytics.Enabled,
93+
Pooler: utils.Config.Db.Pooler.Enabled,
94+
}); err != nil {
95+
return err
96+
}
97+
if err := waitForLocalAPI(ctx, 30*time.Second); err != nil {
6898
return err
6999
}
70100
}
71-
if err := buckets.Run(ctx, "", false, fsys); err != nil {
101+
if err := waitForHealthyService(ctx, 30*time.Second, utils.StorageId); err != nil {
102+
return err
103+
}
104+
if err := seedBuckets(ctx, fsys); err != nil {
72105
return err
73106
}
74107
}
@@ -77,6 +110,13 @@ func Run(ctx context.Context, version string, last uint, config pgconn.Config, f
77110
return nil
78111
}
79112

113+
// shouldRefreshAPIAfterReset returns true when Kong must be recreated after a
114+
// database reset. Apple containers assign dynamic IPs, so Kong's cached
115+
// upstream addresses become stale when the database container is replaced.
116+
func shouldRefreshAPIAfterReset() bool {
117+
return utils.UsesAppleContainerRuntime() && utils.Config.Api.Enabled
118+
}
119+
80120
func resetDatabase(ctx context.Context, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
81121
fmt.Fprintln(os.Stderr, "Resetting local database"+toLogMessage(version))
82122
if utils.Config.Db.MajorVersion <= 14 {
@@ -111,14 +151,14 @@ func resetDatabase14(ctx context.Context, version string, fsys afero.Fs, options
111151
}
112152

113153
func resetDatabase15(ctx context.Context, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
114-
if err := utils.Docker.ContainerRemove(ctx, utils.DbId, container.RemoveOptions{Force: true}); err != nil {
154+
if err := removeContainer(ctx, utils.DbId, true, true); err != nil {
115155
return errors.Errorf("failed to remove container: %w", err)
116156
}
117-
if err := utils.Docker.VolumeRemove(ctx, utils.DbId, true); err != nil {
157+
if err := removeVolume(ctx, utils.DbId, true); err != nil {
118158
return errors.Errorf("failed to remove volume: %w", err)
119159
}
120-
config := start.NewContainerConfig()
121-
hostConfig := start.NewHostConfig()
160+
config := dbstart.NewContainerConfig()
161+
hostConfig := dbstart.NewHostConfig()
122162
networkingConfig := network.NetworkingConfig{
123163
EndpointsConfig: map[string]*network.EndpointSettings{
124164
utils.NetId: {
@@ -127,13 +167,16 @@ func resetDatabase15(ctx context.Context, version string, fsys afero.Fs, options
127167
},
128168
}
129169
fmt.Fprintln(os.Stderr, "Recreating database...")
130-
if _, err := utils.DockerStart(ctx, config, hostConfig, networkingConfig, utils.DbId); err != nil {
170+
if _, err := startContainer(ctx, config, hostConfig, networkingConfig, utils.DbId); err != nil {
131171
return err
132172
}
133-
if err := start.WaitForHealthyService(ctx, utils.Config.Db.HealthTimeout, utils.DbId); err != nil {
173+
if err := waitForHealthyService(ctx, utils.Config.Db.HealthTimeout, utils.DbId); err != nil {
134174
return err
135175
}
136-
if err := start.SetupLocalDatabase(ctx, version, fsys, os.Stderr, options...); err != nil {
176+
if err := waitForLocalDatabase(ctx, utils.Config.Db.HealthTimeout, options...); err != nil {
177+
return err
178+
}
179+
if err := setupLocalDatabase(ctx, version, fsys, os.Stderr, options...); err != nil {
137180
return err
138181
}
139182
fmt.Fprintln(os.Stderr, "Restarting containers...")
@@ -146,7 +189,7 @@ func initDatabase(ctx context.Context, options ...func(*pgx.ConnConfig)) error {
146189
return err
147190
}
148191
defer conn.Close(context.Background())
149-
return start.InitSchema14(ctx, conn)
192+
return dbstart.InitSchema14(ctx, conn)
150193
}
151194

152195
// Recreate postgres database by connecting to template1
@@ -193,7 +236,7 @@ func DisconnectClients(ctx context.Context, conn *pgx.Conn) error {
193236
}
194237
}
195238
// Wait for WAL senders to drop their replication slots
196-
policy := start.NewBackoffPolicy(ctx, 10*time.Second)
239+
policy := dbstart.NewBackoffPolicy(ctx, 10*time.Second)
197240
waitForDrop := func() error {
198241
var count int
199242
if err := conn.QueryRow(ctx, COUNT_REPLICATION_SLOTS).Scan(&count); err != nil {
@@ -211,20 +254,50 @@ func RestartDatabase(ctx context.Context, w io.Writer) error {
211254
fmt.Fprintln(w, "Restarting containers...")
212255
// Some extensions must be manually restarted after pg_terminate_backend
213256
// Ref: https://github.com/citusdata/pg_cron/issues/99
214-
if err := utils.Docker.ContainerRestart(ctx, utils.DbId, container.StopOptions{}); err != nil {
257+
if err := restartContainer(ctx, utils.DbId); err != nil {
215258
return errors.Errorf("failed to restart container: %w", err)
216259
}
217-
if err := start.WaitForHealthyService(ctx, utils.Config.Db.HealthTimeout, utils.DbId); err != nil {
260+
if err := waitForHealthyService(ctx, utils.Config.Db.HealthTimeout, utils.DbId); err != nil {
218261
return err
219262
}
220263
return restartServices(ctx)
221264
}
222265

266+
func waitForDatabaseReady(ctx context.Context, timeout time.Duration, options ...func(*pgx.ConnConfig)) error {
267+
policy := dbstart.NewBackoffPolicy(ctx, timeout)
268+
return backoff.Retry(func() error {
269+
conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{}, options...)
270+
if err != nil {
271+
return err
272+
}
273+
return conn.Close(ctx)
274+
}, policy)
275+
}
276+
277+
func seedBucketsWithRetry(ctx context.Context, fsys afero.Fs) error {
278+
policy := dbstart.NewBackoffPolicy(ctx, 30*time.Second)
279+
return backoff.Retry(func() error {
280+
return runBucketSeed(ctx, "", false, fsys)
281+
}, policy)
282+
}
283+
284+
func waitForAPIReady(ctx context.Context, timeout time.Duration) error {
285+
addr := net.JoinHostPort(utils.Config.Hostname, strconv.FormatUint(uint64(utils.Config.Api.Port), 10))
286+
policy := dbstart.NewBackoffPolicy(ctx, timeout)
287+
return backoff.Retry(func() error {
288+
conn, err := net.DialTimeout("tcp", addr, time.Second)
289+
if err != nil {
290+
return err
291+
}
292+
return conn.Close()
293+
}, policy)
294+
}
295+
223296
func restartServices(ctx context.Context) error {
224297
// No need to restart PostgREST because it automatically reconnects and listens for schema changes
225298
services := listServicesToRestart()
226299
result := utils.WaitAll(services, func(id string) error {
227-
if err := utils.Docker.ContainerRestart(ctx, id, container.StopOptions{}); err != nil && !errdefs.IsNotFound(err) {
300+
if err := restartContainer(ctx, id); err != nil && !errdefs.IsNotFound(err) {
228301
return errors.Errorf("failed to restart %s: %w", id, err)
229302
}
230303
return nil
@@ -233,8 +306,12 @@ func restartServices(ctx context.Context) error {
233306
return errors.Join(result...)
234307
}
235308

309+
// listServicesToRestart returns containers that need restarting after a
310+
// database reset. Kong is included because it caches upstream addresses that
311+
// may change when the database container is recreated (especially on Apple
312+
// containers which use dynamic IPs).
236313
func listServicesToRestart() []string {
237-
return []string{utils.StorageId, utils.GotrueId, utils.RealtimeId, utils.PoolerId}
314+
return []string{utils.StorageId, utils.GotrueId, utils.RealtimeId, utils.PoolerId, utils.KongId}
238315
}
239316

240317
func resetRemote(ctx context.Context, version string, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {

0 commit comments

Comments
 (0)