Skip to content

Commit 43f4766

Browse files
committed
fix: race condition when fetching service versions
1 parent 05ee3e0 commit 43f4766

File tree

2 files changed

+156
-8
lines changed

2 files changed

+156
-8
lines changed

internal/services/services.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ import (
1111
"github.com/supabase/cli/internal/utils"
1212
"github.com/supabase/cli/internal/utils/flags"
1313
"github.com/supabase/cli/internal/utils/tenant"
14+
"github.com/supabase/cli/pkg/config"
1415
"github.com/supabase/cli/pkg/queue"
1516
)
1617

18+
var ErrEnvNotSupported = errors.New("--output env flag is not supported")
19+
1720
func Run(ctx context.Context, fsys afero.Fs) error {
1821
if err := flags.LoadProjectRef(fsys); err != nil && !errors.Is(err, utils.ErrNotLinked) {
1922
fmt.Fprintln(os.Stderr, err)
@@ -43,7 +46,7 @@ func Run(ctx context.Context, fsys afero.Fs) error {
4346
Services: serviceImages,
4447
})
4548
case utils.OutputEnv:
46-
return errors.Errorf("--output env flag is not supported")
49+
return errors.New(ErrEnvNotSupported)
4750
}
4851

4952
return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, serviceImages)
@@ -76,39 +79,39 @@ func CheckVersions(ctx context.Context, fsys afero.Fs) []imageVersion {
7679
}
7780

7881
func listRemoteImages(ctx context.Context, projectRef string) map[string]string {
79-
linked := map[string]string{}
8082
keys, err := tenant.GetApiKeys(ctx, projectRef)
8183
if err != nil {
82-
return linked
84+
return nil
8385
}
86+
linked := config.NewConfig()
8487
jq := queue.NewJobQueue(5)
8588
api := tenant.NewTenantAPI(ctx, projectRef, keys.ServiceRole)
8689
jobs := []func() error{
8790
func() error {
8891
version, err := tenant.GetDatabaseVersion(ctx, projectRef)
8992
if err == nil {
90-
linked[utils.Config.Db.Image] = version
93+
linked.Db.Image = version
9194
}
9295
return nil
9396
},
9497
func() error {
9598
version, err := api.GetGotrueVersion(ctx)
9699
if err == nil {
97-
linked[utils.Config.Auth.Image] = version
100+
linked.Auth.Image = version
98101
}
99102
return nil
100103
},
101104
func() error {
102105
version, err := api.GetPostgrestVersion(ctx)
103106
if err == nil {
104-
linked[utils.Config.Api.Image] = version
107+
linked.Api.Image = version
105108
}
106109
return nil
107110
},
108111
func() error {
109112
version, err := api.GetStorageVersion(ctx)
110113
if err == nil {
111-
linked[utils.Config.Storage.Image] = version
114+
linked.Storage.Image = version
112115
}
113116
return err
114117
},
@@ -123,7 +126,13 @@ func listRemoteImages(ctx context.Context, projectRef string) map[string]string
123126
if err := jq.Collect(); err != nil {
124127
fmt.Fprintln(logger, err)
125128
}
126-
return linked
129+
// Convert to map last to avoid race condition
130+
return map[string]string{
131+
utils.Config.Db.Image: linked.Db.Image,
132+
utils.Config.Auth.Image: linked.Auth.Image,
133+
utils.Config.Api.Image: linked.Api.Image,
134+
utils.Config.Storage.Image: linked.Storage.Image,
135+
}
127136
}
128137

129138
func suggestUpdateCmd(serviceImages map[string]string) string {

internal/services/services_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package services
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/h2non/gock"
9+
"github.com/oapi-codegen/nullable"
10+
"github.com/spf13/afero"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/supabase/cli/internal/testing/apitest"
13+
"github.com/supabase/cli/internal/utils"
14+
"github.com/supabase/cli/internal/utils/flags"
15+
"github.com/supabase/cli/internal/utils/tenant"
16+
"github.com/supabase/cli/pkg/api"
17+
)
18+
19+
func TestServicesCommand(t *testing.T) {
20+
t.Run("output pretty", func(t *testing.T) {
21+
utils.OutputFormat.Value = utils.OutputPretty
22+
// Run test
23+
err := Run(context.Background(), afero.NewMemMapFs())
24+
// Check error
25+
assert.NoError(t, err)
26+
})
27+
28+
t.Run("output toml", func(t *testing.T) {
29+
utils.OutputFormat.Value = utils.OutputToml
30+
// Run test
31+
err := Run(context.Background(), afero.NewMemMapFs())
32+
// Check error
33+
assert.NoError(t, err)
34+
})
35+
36+
t.Run("output json", func(t *testing.T) {
37+
utils.OutputFormat.Value = utils.OutputJson
38+
// Run test
39+
err := Run(context.Background(), afero.NewMemMapFs())
40+
// Check error
41+
assert.NoError(t, err)
42+
})
43+
44+
t.Run("output env", func(t *testing.T) {
45+
utils.OutputFormat.Value = utils.OutputEnv
46+
// Run test
47+
err := Run(context.Background(), afero.NewMemMapFs())
48+
// Check error
49+
assert.ErrorIs(t, err, ErrEnvNotSupported)
50+
})
51+
}
52+
53+
func TestCheckVersions(t *testing.T) {
54+
// Setup valid access token
55+
token := apitest.RandomAccessToken(t)
56+
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
57+
// Setup valid project ref
58+
flags.ProjectRef = apitest.RandomProjectRef()
59+
projectHost := "https://" + utils.GetSupabaseHost(flags.ProjectRef)
60+
// Setup mock project
61+
mockProject := api.V1ProjectWithDatabaseResponse{}
62+
mockProject.Database.Version = "14.1.0.99"
63+
64+
t.Run("diff service versions", func(t *testing.T) {
65+
// Setup mock api
66+
defer gock.OffAll()
67+
gock.New(utils.DefaultApiHost).
68+
Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
69+
Reply(http.StatusOK).
70+
JSON([]api.ApiKeyResponse{{
71+
Name: "service_role",
72+
ApiKey: nullable.NewNullableWithValue("service-key"),
73+
}})
74+
gock.New(utils.DefaultApiHost).
75+
Get("/v1/projects/" + flags.ProjectRef).
76+
Reply(http.StatusOK).
77+
JSON(mockProject)
78+
// Mock service versions
79+
gock.New(projectHost).
80+
Get("/auth/v1/health").
81+
Reply(http.StatusOK).
82+
JSON(tenant.HealthResponse{Version: "v2.74.2"})
83+
gock.New(projectHost).
84+
Get("/rest/v1/").
85+
Reply(http.StatusOK).
86+
JSON(tenant.SwaggerResponse{Info: tenant.SwaggerInfo{Version: "11.1.0"}})
87+
gock.New(projectHost).
88+
Get("/storage/v1/version").
89+
Reply(http.StatusOK).
90+
BodyString("1.28.0")
91+
// Run test
92+
images := CheckVersions(context.Background(), afero.NewMemMapFs())
93+
// Check error
94+
assert.Equal(t, len(images), 10)
95+
for _, img := range images {
96+
assert.NotEqual(t, img.Local, img.Remote)
97+
}
98+
assert.Empty(t, apitest.ListUnmatchedRequests())
99+
})
100+
101+
t.Run("list remote images", func(t *testing.T) {
102+
// Setup mock api
103+
defer gock.OffAll()
104+
gock.New(utils.DefaultApiHost).
105+
Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
106+
Reply(http.StatusOK).
107+
JSON([]api.ApiKeyResponse{{
108+
Name: "service_role",
109+
ApiKey: nullable.NewNullableWithValue("service-key"),
110+
}})
111+
gock.New(utils.DefaultApiHost).
112+
Get("/v1/projects/" + flags.ProjectRef).
113+
Reply(http.StatusOK).
114+
JSON(mockProject)
115+
// Mock service versions
116+
gock.New(projectHost).
117+
Get("/auth/v1/health").
118+
Reply(http.StatusOK).
119+
JSON(tenant.HealthResponse{Version: "v2.74.2"})
120+
gock.New(projectHost).
121+
Get("/rest/v1/").
122+
Reply(http.StatusOK).
123+
JSON(tenant.SwaggerResponse{Info: tenant.SwaggerInfo{Version: "11.1.0"}})
124+
gock.New(projectHost).
125+
Get("/storage/v1/version").
126+
Reply(http.StatusOK).
127+
BodyString("1.28.0")
128+
// Run test
129+
images := listRemoteImages(context.Background(), flags.ProjectRef)
130+
// Check error
131+
assert.Equal(t, images, map[string]string{
132+
utils.Config.Db.Image: "14.1.0.99",
133+
utils.Config.Auth.Image: "v2.74.2",
134+
utils.Config.Api.Image: "v11.1.0",
135+
utils.Config.Storage.Image: "v1.28.0",
136+
})
137+
assert.Empty(t, apitest.ListUnmatchedRequests())
138+
})
139+
}

0 commit comments

Comments
 (0)