• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / cli / 15585019893

11 Jun 2025 12:35PM UTC coverage: 55.511% (+0.4%) from 55.076%
15585019893

Pull #3675

github

web-flow
Merge aa640d28f into b3bee3dd5
Pull Request #3675: feat: hot reload eszip bundle when function source changes

368 of 536 new or added lines in 8 files covered. (68.66%)

9 existing lines in 2 files now uncovered.

6280 of 11313 relevant lines covered (55.51%)

7.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.29
/internal/functions/serve/service.go
1
package serve
2

3
import (
4
        "context"
5
        _ "embed"
6
        "encoding/json"
7
        "fmt"
8
        "log"
9
        "os"
10
        "path/filepath"
11
        "strconv"
12
        "strings"
13

14
        "github.com/docker/docker/api/types/container"
15
        "github.com/docker/docker/api/types/network"
16
        "github.com/docker/go-connections/nat"
17
        "github.com/go-errors/errors"
18
        "github.com/spf13/afero"
19
        "github.com/spf13/viper"
20
        "github.com/supabase/cli/internal/functions/deploy"
21
        "github.com/supabase/cli/internal/secrets/set"
22
        "github.com/supabase/cli/internal/utils"
23
)
24

25
const (
26
        dockerRuntimeServerPort = 8081
27
)
28

29
//go:embed templates/main.ts
30
var mainFuncEmbed string
31

32
// manageFunctionServices handles the lifecycle of serving functions and streaming logs.
33
// It returns a context cancellation function for the log streaming and a channel that closes when log streaming is done.
34
func manageFunctionServices(
35
        ctx context.Context,
36
        envFilePath string,
37
        noVerifyJWT *bool,
38
        importMapPath string,
39
        dbUrl string,
40
        runtimeOption RuntimeOption,
41
        fsys afero.Fs,
42
        errChan chan<- error,
43
) (context.CancelFunc, <-chan struct{}, error) {
8✔
44
        // Remove existing container before starting a new one.
8✔
45
        if err := utils.Docker.ContainerRemove(context.Background(), utils.EdgeRuntimeId, container.RemoveOptions{
8✔
46
                RemoveVolumes: true,
8✔
47
                Force:         true,
8✔
48
        }); err != nil {
10✔
49
                log.Println("Warning: Failed to remove existing Edge Runtime container before start:", err)
2✔
50
        }
2✔
51

52
        fmt.Fprintln(os.Stderr, "Setting up Edge Functions runtime...")
8✔
53
        // Create a new context for ServeFunctions and DockerStreamLogs that can be cancelled independently for restarts.
8✔
54
        serviceCtx, serviceCancel := context.WithCancel(ctx)
8✔
55

8✔
56
        if err := ServeFunctions(serviceCtx, envFilePath, noVerifyJWT, importMapPath, dbUrl, runtimeOption, fsys); err != nil {
11✔
57
                // Clean up the context if ServeFunctions fails
3✔
58
                serviceCancel()
3✔
59
                return nil, nil, errors.Errorf("Failed to serve functions: %w", err)
3✔
60
        }
3✔
61

62
        fmt.Fprintln(os.Stderr, "Edge Functions runtime is ready.")
5✔
63

5✔
64
        // To signal completion of log streaming
5✔
65
        logsDone := make(chan struct{})
5✔
66
        go func() {
10✔
67
                defer close(logsDone)
5✔
68
                // Ensure cancel is called if DockerStreamLogs returns or panics
5✔
69
                defer serviceCancel()
5✔
70

5✔
71
                if logErr := utils.DockerStreamLogs(serviceCtx, utils.EdgeRuntimeId, os.Stdout, os.Stderr); logErr != nil {
9✔
72
                        if !errors.Is(logErr, context.Canceled) && !strings.Contains(logErr.Error(), "context canceled") {
6✔
73
                                select {
2✔
74
                                case errChan <- errors.Errorf("Docker log streaming error: %w", logErr):
1✔
75
                                default: // Avoid blocking if errChan is full
1✔
76
                                        log.Println("Error channel full, dropping Docker log streaming error:", logErr)
1✔
77
                                }
78
                        }
79
                }
80
        }()
81

82
        return serviceCancel, logsDone, nil
5✔
83
}
84

85
func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, dbUrl string, runtimeOption RuntimeOption, fsys afero.Fs) error {
11✔
86
        // 1. Parse custom env file
11✔
87
        env, err := parseEnvFile(envFilePath, fsys)
11✔
88
        if err != nil {
15✔
89
                return err
4✔
90
        }
4✔
91
        env = append(env,
7✔
92
                fmt.Sprintf("SUPABASE_URL=http://%s:8000", utils.KongAliases[0]),
7✔
93
                "SUPABASE_ANON_KEY="+utils.Config.Auth.AnonKey.Value,
7✔
94
                "SUPABASE_SERVICE_ROLE_KEY="+utils.Config.Auth.ServiceRoleKey.Value,
7✔
95
                "SUPABASE_DB_URL="+dbUrl,
7✔
96
                "SUPABASE_INTERNAL_JWT_SECRET="+utils.Config.Auth.JwtSecret.Value,
7✔
97
                fmt.Sprintf("SUPABASE_INTERNAL_HOST_PORT=%d", utils.Config.Api.Port),
7✔
98
        )
7✔
99
        if viper.GetBool("DEBUG") {
7✔
NEW
100
                env = append(env, "SUPABASE_INTERNAL_DEBUG=true")
×
NEW
101
        }
×
102
        if runtimeOption.InspectMode != nil {
7✔
NEW
103
                env = append(env, "SUPABASE_INTERNAL_WALLCLOCK_LIMIT_SEC=0")
×
NEW
104
        }
×
105
        // 2. Parse custom import map
106
        cwd, err := os.Getwd()
7✔
107
        if err != nil {
7✔
NEW
108
                return errors.Errorf("failed to get working directory: %w", err)
×
NEW
109
        }
×
110
        if len(importMapPath) > 0 {
7✔
NEW
111
                if !filepath.IsAbs(importMapPath) {
×
NEW
112
                        importMapPath = filepath.Join(utils.CurrentDirAbs, importMapPath)
×
NEW
113
                }
×
NEW
114
                if importMapPath, err = filepath.Rel(cwd, importMapPath); err != nil {
×
NEW
115
                        return errors.Errorf("failed to resolve relative path: %w", err)
×
NEW
116
                }
×
117
        }
118
        binds, functionsConfigString, err := populatePerFunctionConfigs(cwd, importMapPath, noVerifyJWT, fsys)
7✔
119
        if err != nil {
7✔
NEW
120
                return err
×
NEW
121
        }
×
122
        env = append(env, "SUPABASE_INTERNAL_FUNCTIONS_CONFIG="+functionsConfigString)
7✔
123
        // 3. Parse entrypoint script
7✔
124
        cmd := append([]string{
7✔
125
                "edge-runtime",
7✔
126
                "start",
7✔
127
                "--main-service=/root",
7✔
128
                fmt.Sprintf("--port=%d", dockerRuntimeServerPort),
7✔
129
                fmt.Sprintf("--policy=%s", utils.Config.EdgeRuntime.Policy),
7✔
130
        }, runtimeOption.toArgs()...)
7✔
131
        if viper.GetBool("DEBUG") {
7✔
NEW
132
                cmd = append(cmd, "--verbose")
×
NEW
133
        }
×
134
        cmdString := strings.Join(cmd, " ")
7✔
135
        entrypoint := []string{"sh", "-c", `cat <<'EOF' > /root/index.ts && ` + cmdString + `
7✔
136
` + mainFuncEmbed + `
7✔
137
EOF
7✔
138
`}
7✔
139
        // 4. Parse exposed ports
7✔
140
        dockerRuntimePort := nat.Port(fmt.Sprintf("%d/tcp", dockerRuntimeServerPort))
7✔
141
        exposedPorts := nat.PortSet{dockerRuntimePort: struct{}{}}
7✔
142
        portBindings := nat.PortMap{}
7✔
143
        if runtimeOption.InspectMode != nil {
7✔
NEW
144
                // dockerRuntimeInspectorPort is kept in serve.go as it's used by RuntimeOption
×
NEW
145
                dockerInspectorPort := nat.Port(fmt.Sprintf("%d/tcp", dockerRuntimeInspectorPort))
×
NEW
146
                exposedPorts[dockerInspectorPort] = struct{}{}
×
NEW
147
                portBindings[dockerInspectorPort] = []nat.PortBinding{{
×
NEW
148
                        HostPort: strconv.FormatUint(uint64(utils.Config.EdgeRuntime.InspectorPort), 10),
×
NEW
149
                }}
×
NEW
150
        }
×
151
        // 5. Start container
152
        _, err = utils.DockerStart(
7✔
153
                ctx,
7✔
154
                container.Config{
7✔
155
                        Image:        utils.Config.EdgeRuntime.Image,
7✔
156
                        Env:          env,
7✔
157
                        Entrypoint:   entrypoint,
7✔
158
                        ExposedPorts: exposedPorts,
7✔
159
                        WorkingDir:   utils.ToDockerPath(cwd),
7✔
160
                },
7✔
161
                container.HostConfig{
7✔
162
                        Binds:        binds,
7✔
163
                        PortBindings: portBindings,
7✔
164
                },
7✔
165
                network.NetworkingConfig{
7✔
166
                        EndpointsConfig: map[string]*network.EndpointSettings{
7✔
167
                                utils.NetId: {
7✔
168
                                        Aliases: utils.EdgeRuntimeAliases,
7✔
169
                                },
7✔
170
                        },
7✔
171
                },
7✔
172
                utils.EdgeRuntimeId,
7✔
173
        )
7✔
174
        return err
7✔
175
}
176

177
func parseEnvFile(envFilePath string, fsys afero.Fs) ([]string, error) {
14✔
178
        if envFilePath == "" {
22✔
179
                if f, err := fsys.Stat(utils.FallbackEnvFilePath); err == nil && !f.IsDir() {
10✔
180
                        envFilePath = utils.FallbackEnvFilePath
2✔
181
                }
2✔
182
        } else if !filepath.IsAbs(envFilePath) {
10✔
183
                envFilePath = filepath.Join(utils.CurrentDirAbs, envFilePath)
4✔
184
        }
4✔
185
        env := []string{}
14✔
186
        secrets, err := set.ListSecrets(envFilePath, fsys)
14✔
187
        if err != nil {
19✔
188
                // If parsing fails, return empty slice and error
5✔
189
                return nil, err
5✔
190
        }
5✔
191
        for _, v := range secrets {
13✔
192
                env = append(env, fmt.Sprintf("%s=%s", v.Name, v.Value))
4✔
193
        }
4✔
194
        return env, nil
9✔
195
}
196

197
func populatePerFunctionConfigs(cwd, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) ([]string, string, error) {
11✔
198
        slugs, err := deploy.GetFunctionSlugs(fsys)
11✔
199
        if err != nil {
11✔
NEW
200
                return nil, "", err
×
NEW
201
        }
×
202
        functionsConfig, err := deploy.GetFunctionConfig(slugs, importMapPath, noVerifyJWT, fsys)
11✔
203
        if err != nil {
11✔
NEW
204
                return nil, "", err
×
NEW
205
        }
×
206
        binds := []string{}
11✔
207
        for slug, fc := range functionsConfig {
14✔
208
                if !fc.Enabled {
3✔
NEW
209
                        fmt.Fprintln(os.Stderr, "Skipped serving Function:", slug)
×
NEW
210
                        continue
×
211
                }
212
                modules, err := deploy.GetBindMounts(cwd, utils.FunctionsDir, "", fc.Entrypoint, fc.ImportMap, fsys)
3✔
213
                if err != nil {
3✔
NEW
214
                        return nil, "", err
×
NEW
215
                }
×
216
                binds = append(binds, modules...)
3✔
217
                fc.ImportMap = utils.ToDockerPath(fc.ImportMap)
3✔
218
                fc.Entrypoint = utils.ToDockerPath(fc.Entrypoint)
3✔
219
                // Update the map with docker paths
3✔
220
                for i, val := range fc.StaticFiles {
3✔
NEW
221
                        fc.StaticFiles[i] = utils.ToDockerPath(val)
×
NEW
222
                }
×
223
                functionsConfig[slug] = fc
3✔
224
        }
225
        functionsConfigBytes, err := json.Marshal(functionsConfig)
11✔
226
        if err != nil {
11✔
NEW
227
                return nil, "", errors.Errorf("failed to marshal config json: %w", err)
×
NEW
228
        }
×
229
        return utils.RemoveDuplicates(binds), string(functionsConfigBytes), nil
11✔
230
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc