Skip to content

Commit 274173a

Browse files
committed
fix: return deploy metadata from bundle
1 parent c994c16 commit 274173a

File tree

7 files changed

+225
-93
lines changed

7 files changed

+225
-93
lines changed

internal/functions/deploy/bundle.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/spf13/afero"
1515
"github.com/spf13/viper"
1616
"github.com/supabase/cli/internal/utils"
17+
"github.com/supabase/cli/pkg/api"
1718
"github.com/supabase/cli/pkg/function"
1819
)
1920

@@ -25,17 +26,17 @@ func NewDockerBundler(fsys afero.Fs) function.EszipBundler {
2526
return &dockerBundler{fsys: fsys}
2627
}
2728

28-
func (b *dockerBundler) Bundle(ctx context.Context, slug, entrypoint, importMap string, staticFiles []string, output io.Writer) error {
29-
// Create temp directory to store generated eszip
29+
func (b *dockerBundler) Bundle(ctx context.Context, slug, entrypoint, importMap string, staticFiles []string, output io.Writer) (api.FunctionDeployMetadata, error) {
30+
meta := function.NewMetadata(slug, entrypoint, importMap, staticFiles)
3031
fmt.Fprintln(os.Stderr, "Bundling Function:", utils.Bold(slug))
3132
cwd, err := os.Getwd()
3233
if err != nil {
33-
return errors.Errorf("failed to get working directory: %w", err)
34+
return meta, errors.Errorf("failed to get working directory: %w", err)
3435
}
3536
// BitBucket pipelines require docker bind mounts to be world writable
3637
hostOutputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug))
3738
if err := b.fsys.MkdirAll(hostOutputDir, 0777); err != nil {
38-
return errors.Errorf("failed to mkdir: %w", err)
39+
return meta, errors.Errorf("failed to mkdir: %w", err)
3940
}
4041
defer func() {
4142
if err := b.fsys.RemoveAll(hostOutputDir); err != nil {
@@ -45,16 +46,16 @@ func (b *dockerBundler) Bundle(ctx context.Context, slug, entrypoint, importMap
4546
// Create bind mounts
4647
binds, err := GetBindMounts(cwd, utils.FunctionsDir, hostOutputDir, entrypoint, importMap, b.fsys)
4748
if err != nil {
48-
return err
49+
return meta, err
4950
}
5051
hostOutputPath := filepath.Join(hostOutputDir, "output.eszip")
5152
// Create exec command
5253
cmd := []string{"bundle", "--entrypoint", utils.ToDockerPath(entrypoint), "--output", utils.ToDockerPath(hostOutputPath)}
5354
if len(importMap) > 0 {
5455
cmd = append(cmd, "--import-map", utils.ToDockerPath(importMap))
5556
}
56-
for _, staticFile := range staticFiles {
57-
cmd = append(cmd, "--static", utils.ToDockerPath(staticFile))
57+
for _, sf := range staticFiles {
58+
cmd = append(cmd, "--static", utils.ToDockerPath(sf))
5859
}
5960
if viper.GetBool("DEBUG") {
6061
cmd = append(cmd, "--verbose")
@@ -81,15 +82,15 @@ func (b *dockerBundler) Bundle(ctx context.Context, slug, entrypoint, importMap
8182
os.Stdout,
8283
os.Stderr,
8384
); err != nil {
84-
return err
85+
return meta, err
8586
}
8687
// Read and compress
8788
eszipBytes, err := b.fsys.Open(hostOutputPath)
8889
if err != nil {
89-
return errors.Errorf("failed to open eszip: %w", err)
90+
return meta, errors.Errorf("failed to open eszip: %w", err)
9091
}
9192
defer eszipBytes.Close()
92-
return function.Compress(eszipBytes, output)
93+
return meta, function.Compress(eszipBytes, output)
9394
}
9495

9596
func GetBindMounts(cwd, hostFuncDir, hostOutputDir, hostEntrypointPath, hostImportMapPath string, fsys afero.Fs) ([]string, error) {

internal/functions/deploy/bundle_test.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import (
44
"archive/zip"
55
"bytes"
66
"context"
7+
"fmt"
78
"net/http"
9+
"os"
10+
"path/filepath"
811
"testing"
912

1013
"github.com/h2non/gock"
@@ -13,16 +16,21 @@ import (
1316
"github.com/stretchr/testify/require"
1417
"github.com/supabase/cli/internal/testing/apitest"
1518
"github.com/supabase/cli/internal/utils"
19+
"github.com/supabase/cli/pkg/cast"
1620
)
1721

1822
func TestDockerBundle(t *testing.T) {
1923
imageUrl := utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image)
2024
utils.EdgeRuntimeId = "test-edge-runtime"
2125
const containerId = "test-container"
26+
cwd, err := os.Getwd()
27+
require.NoError(t, err)
2228

2329
t.Run("throws error on bundle failure", func(t *testing.T) {
2430
// Setup in-memory fs
2531
fsys := afero.NewMemMapFs()
32+
absImportMap := filepath.Join(cwd, "hello", "deno.json")
33+
require.NoError(t, utils.WriteFile(absImportMap, []byte("{}"), fsys))
2634
// Setup deno error
2735
t.Setenv("TEST_DENO_ERROR", "bundle failed")
2836
var body bytes.Buffer
@@ -42,10 +50,51 @@ func TestDockerBundle(t *testing.T) {
4250
require.NoError(t, apitest.MockDocker(utils.Docker))
4351
apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
4452
require.NoError(t, apitest.MockDockerLogsExitCode(utils.Docker, containerId, 1))
53+
// Setup mock bundler
54+
bundler := NewDockerBundler(fsys)
4555
// Run test
46-
err = NewDockerBundler(fsys).Bundle(context.Background(), "", "", "", []string{}, &body)
56+
meta, err := bundler.Bundle(
57+
context.Background(),
58+
"hello",
59+
filepath.Join("hello", "index.ts"),
60+
filepath.Join("hello", "deno.json"),
61+
[]string{filepath.Join("hello", "data.pdf")},
62+
&body,
63+
)
4764
// Check error
4865
assert.ErrorContains(t, err, "error running container: exit 1")
4966
assert.Empty(t, apitest.ListUnmatchedRequests())
67+
assert.Equal(t, cast.Ptr("hello"), meta.Name)
68+
entrypoint := fmt.Sprintf("file://%s/hello/index.ts", filepath.ToSlash(cwd))
69+
assert.Equal(t, entrypoint, meta.EntrypointPath)
70+
importMap := fmt.Sprintf("file://%s/hello/deno.json", filepath.ToSlash(cwd))
71+
assert.Equal(t, &importMap, meta.ImportMapPath)
72+
staticFile := fmt.Sprintf("file://%s/hello/data.pdf", filepath.ToSlash(cwd))
73+
assert.Equal(t, cast.Ptr([]string{staticFile}), meta.StaticPatterns)
74+
assert.Nil(t, meta.VerifyJwt)
75+
})
76+
77+
t.Run("throws error on permission denied", func(t *testing.T) {
78+
// Setup in-memory fs
79+
fsys := afero.NewReadOnlyFs(afero.NewMemMapFs())
80+
// Setup mock bundler
81+
bundler := NewDockerBundler(fsys)
82+
// Run test
83+
meta, err := bundler.Bundle(
84+
context.Background(),
85+
"hello",
86+
"hello/index.ts",
87+
"",
88+
nil,
89+
nil,
90+
)
91+
// Check error
92+
assert.ErrorIs(t, err, os.ErrPermission)
93+
assert.Equal(t, cast.Ptr("hello"), meta.Name)
94+
entrypoint := fmt.Sprintf("file://%s/hello/index.ts", filepath.ToSlash(cwd))
95+
assert.Equal(t, entrypoint, meta.EntrypointPath)
96+
assert.Nil(t, meta.ImportMapPath)
97+
assert.NotNil(t, meta.StaticPatterns)
98+
assert.Nil(t, meta.VerifyJwt)
5099
})
51100
}

pkg/function/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type EdgeRuntimeAPI struct {
1515
}
1616

1717
type EszipBundler interface {
18-
Bundle(ctx context.Context, slug, entrypoint, importMap string, staticFiles []string, output io.Writer) error
18+
Bundle(ctx context.Context, slug, entrypoint, importMap string, staticFiles []string, output io.Writer) (api.FunctionDeployMetadata, error)
1919
}
2020

2121
func NewEdgeRuntimeAPI(project string, client api.ClientWithResponses, opts ...withOption) EdgeRuntimeAPI {

pkg/function/batch.go

Lines changed: 59 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7-
"net/url"
7+
"io"
88
"os"
9-
"path/filepath"
10-
"strings"
119

1210
"github.com/cenkalti/backoff/v4"
1311
"github.com/docker/go-units"
@@ -26,15 +24,15 @@ func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig con
2624
if resp, err := s.client.V1ListAllFunctionsWithResponse(ctx, s.project); err != nil {
2725
return errors.Errorf("failed to list functions: %w", err)
2826
} else if resp.JSON200 == nil {
29-
return errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body))
27+
return errors.Errorf("unexpected list functions status %d: %s", resp.StatusCode(), string(resp.Body))
3028
} else {
3129
result = *resp.JSON200
3230
}
3331
exists := make(map[string]struct{}, len(result))
3432
for _, f := range result {
3533
exists[f.Slug] = struct{}{}
3634
}
37-
toUpdate := map[string]api.BulkUpdateFunctionBody{}
35+
var toUpdate []api.BulkUpdateFunctionBody
3836
OUTER:
3937
for slug, function := range functionConfig {
4038
if !function.Enabled {
@@ -47,75 +45,29 @@ OUTER:
4745
}
4846
}
4947
var body bytes.Buffer
50-
if err := s.eszip.Bundle(ctx, slug, function.Entrypoint, function.ImportMap, function.StaticFiles, &body); err != nil {
48+
meta, err := s.eszip.Bundle(ctx, slug, function.Entrypoint, function.ImportMap, function.StaticFiles, &body)
49+
if err != nil {
5150
return err
5251
}
52+
meta.VerifyJwt = &function.VerifyJWT
5353
// Update if function already exists
54-
upsert := func() error {
54+
upsert := func() (api.BulkUpdateFunctionBody, error) {
5555
if _, ok := exists[slug]; ok {
56-
resp, err := s.client.V1UpdateAFunctionWithBodyWithResponse(ctx, s.project, slug, &api.V1UpdateAFunctionParams{
57-
VerifyJwt: &function.VerifyJWT,
58-
ImportMapPath: toFileURL(function.ImportMap),
59-
EntrypointPath: toFileURL(function.Entrypoint),
60-
}, eszipContentType, bytes.NewReader(body.Bytes()))
61-
if err != nil {
62-
return errors.Errorf("failed to update function: %w", err)
63-
} else if resp.JSON200 == nil {
64-
return errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body))
65-
}
66-
toUpdate[slug] = api.BulkUpdateFunctionBody{
67-
Id: resp.JSON200.Id,
68-
Name: resp.JSON200.Name,
69-
Slug: resp.JSON200.Slug,
70-
Version: resp.JSON200.Version,
71-
EntrypointPath: resp.JSON200.EntrypointPath,
72-
ImportMap: resp.JSON200.ImportMap,
73-
ImportMapPath: resp.JSON200.ImportMapPath,
74-
VerifyJwt: resp.JSON200.VerifyJwt,
75-
Status: api.BulkUpdateFunctionBodyStatus(resp.JSON200.Status),
76-
CreatedAt: &resp.JSON200.CreatedAt,
77-
}
78-
} else {
79-
resp, err := s.client.V1CreateAFunctionWithBodyWithResponse(ctx, s.project, &api.V1CreateAFunctionParams{
80-
Slug: &slug,
81-
Name: &slug,
82-
VerifyJwt: &function.VerifyJWT,
83-
ImportMapPath: toFileURL(function.ImportMap),
84-
EntrypointPath: toFileURL(function.Entrypoint),
85-
}, eszipContentType, bytes.NewReader(body.Bytes()))
86-
if err != nil {
87-
return errors.Errorf("failed to create function: %w", err)
88-
} else if resp.JSON201 == nil {
89-
return errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body))
90-
}
91-
toUpdate[slug] = api.BulkUpdateFunctionBody{
92-
Id: resp.JSON201.Id,
93-
Name: resp.JSON201.Name,
94-
Slug: resp.JSON201.Slug,
95-
Version: resp.JSON201.Version,
96-
EntrypointPath: resp.JSON201.EntrypointPath,
97-
ImportMap: resp.JSON201.ImportMap,
98-
ImportMapPath: resp.JSON201.ImportMapPath,
99-
VerifyJwt: resp.JSON201.VerifyJwt,
100-
Status: api.BulkUpdateFunctionBodyStatus(resp.JSON201.Status),
101-
CreatedAt: &resp.JSON201.CreatedAt,
102-
}
56+
return s.updateFunction(ctx, slug, meta, bytes.NewReader(body.Bytes()))
10357
}
104-
return nil
58+
return s.createFunction(ctx, slug, meta, bytes.NewReader(body.Bytes()))
10559
}
10660
functionSize := units.HumanSize(float64(body.Len()))
10761
fmt.Fprintf(os.Stderr, "Deploying Function: %s (script size: %s)\n", slug, functionSize)
10862
policy := backoff.WithContext(backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries), ctx)
109-
if err := backoff.Retry(upsert, policy); err != nil {
63+
result, err := backoff.RetryWithData(upsert, policy)
64+
if err != nil {
11065
return err
11166
}
67+
toUpdate = append(toUpdate, result)
11268
}
11369
if len(toUpdate) > 1 {
114-
var body []api.BulkUpdateFunctionBody
115-
for _, b := range toUpdate {
116-
body = append(body, b)
117-
}
118-
if resp, err := s.client.V1BulkUpdateFunctionsWithResponse(ctx, s.project, body); err != nil {
70+
if resp, err := s.client.V1BulkUpdateFunctionsWithResponse(ctx, s.project, toUpdate); err != nil {
11971
return errors.Errorf("failed to bulk update: %w", err)
12072
} else if resp.JSON200 == nil {
12173
return errors.Errorf("unexpected bulk update status %d: %s", resp.StatusCode(), string(resp.Body))
@@ -124,19 +76,54 @@ OUTER:
12476
return nil
12577
}
12678

127-
func toFileURL(hostPath string) *string {
128-
absHostPath, err := filepath.Abs(hostPath)
79+
func (s *EdgeRuntimeAPI) updateFunction(ctx context.Context, slug string, meta api.FunctionDeployMetadata, body io.Reader) (api.BulkUpdateFunctionBody, error) {
80+
resp, err := s.client.V1UpdateAFunctionWithBodyWithResponse(ctx, s.project, slug, &api.V1UpdateAFunctionParams{
81+
VerifyJwt: meta.VerifyJwt,
82+
ImportMapPath: meta.ImportMapPath,
83+
EntrypointPath: &meta.EntrypointPath,
84+
}, eszipContentType, body)
12985
if err != nil {
130-
return nil
86+
return api.BulkUpdateFunctionBody{}, errors.Errorf("failed to update function: %w", err)
87+
} else if resp.JSON200 == nil {
88+
return api.BulkUpdateFunctionBody{}, errors.Errorf("unexpected update function status %d: %s", resp.StatusCode(), string(resp.Body))
13189
}
132-
// Convert to unix path because edge runtime only supports linux
133-
parsed := url.URL{Scheme: "file", Path: toUnixPath(absHostPath)}
134-
result := parsed.String()
135-
return &result
90+
return api.BulkUpdateFunctionBody{
91+
Id: resp.JSON200.Id,
92+
Name: resp.JSON200.Name,
93+
Slug: resp.JSON200.Slug,
94+
Version: resp.JSON200.Version,
95+
EntrypointPath: resp.JSON200.EntrypointPath,
96+
ImportMap: resp.JSON200.ImportMap,
97+
ImportMapPath: resp.JSON200.ImportMapPath,
98+
VerifyJwt: resp.JSON200.VerifyJwt,
99+
Status: api.BulkUpdateFunctionBodyStatus(resp.JSON200.Status),
100+
CreatedAt: &resp.JSON200.CreatedAt,
101+
}, nil
136102
}
137103

138-
func toUnixPath(absHostPath string) string {
139-
prefix := filepath.VolumeName(absHostPath)
140-
unixPath := filepath.ToSlash(absHostPath)
141-
return strings.TrimPrefix(unixPath, prefix)
104+
func (s *EdgeRuntimeAPI) createFunction(ctx context.Context, slug string, meta api.FunctionDeployMetadata, body io.Reader) (api.BulkUpdateFunctionBody, error) {
105+
resp, err := s.client.V1CreateAFunctionWithBodyWithResponse(ctx, s.project, &api.V1CreateAFunctionParams{
106+
Slug: &slug,
107+
Name: &slug,
108+
VerifyJwt: meta.VerifyJwt,
109+
ImportMapPath: meta.ImportMapPath,
110+
EntrypointPath: &meta.EntrypointPath,
111+
}, eszipContentType, body)
112+
if err != nil {
113+
return api.BulkUpdateFunctionBody{}, errors.Errorf("failed to create function: %w", err)
114+
} else if resp.JSON201 == nil {
115+
return api.BulkUpdateFunctionBody{}, errors.Errorf("unexpected create function status %d: %s", resp.StatusCode(), string(resp.Body))
116+
}
117+
return api.BulkUpdateFunctionBody{
118+
Id: resp.JSON201.Id,
119+
Name: resp.JSON201.Name,
120+
Slug: resp.JSON201.Slug,
121+
Version: resp.JSON201.Version,
122+
EntrypointPath: resp.JSON201.EntrypointPath,
123+
ImportMap: resp.JSON201.ImportMap,
124+
ImportMapPath: resp.JSON201.ImportMapPath,
125+
VerifyJwt: resp.JSON201.VerifyJwt,
126+
Status: api.BulkUpdateFunctionBodyStatus(resp.JSON201.Status),
127+
CreatedAt: &resp.JSON201.CreatedAt,
128+
}, nil
142129
}

pkg/function/batch_test.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,16 @@ import (
1717
type MockBundler struct {
1818
}
1919

20-
func (b *MockBundler) Bundle(ctx context.Context, slug, entrypoint, importMap string, staticFiles []string, output io.Writer) error {
21-
return nil
20+
func (b *MockBundler) Bundle(ctx context.Context, slug, entrypoint, importMap string, staticFiles []string, output io.Writer) (api.FunctionDeployMetadata, error) {
21+
if staticFiles == nil {
22+
staticFiles = []string{}
23+
}
24+
return api.FunctionDeployMetadata{
25+
Name: &slug,
26+
EntrypointPath: entrypoint,
27+
ImportMapPath: &importMap,
28+
StaticPatterns: &staticFiles,
29+
}, nil
2230
}
2331

2432
const (
@@ -54,7 +62,7 @@ func TestUpsertFunctions(t *testing.T) {
5462
// Run test
5563
err := client.UpsertFunctions(context.Background(), nil)
5664
// Check error
57-
assert.ErrorContains(t, err, "unexpected status 503:")
65+
assert.ErrorContains(t, err, "unexpected list functions status 503:")
5866
})
5967

6068
t.Run("retries on create failure", func(t *testing.T) {

0 commit comments

Comments
 (0)