From 614b461418389ad4e5d615f719c06b3dd0080400 Mon Sep 17 00:00:00 2001 From: Lakshan Perera Date: Tue, 24 Dec 2024 12:18:28 +1100 Subject: [PATCH 1/2] feat: support passing `--env-file` flag to functions deploy command. --- cmd/functions.go | 5 +++-- examples/functions-deploy/main.go | 2 +- internal/functions/deploy/bundle.go | 18 ++++++++++++++++-- internal/functions/deploy/bundle_test.go | 2 +- internal/functions/deploy/deploy.go | 4 ++-- internal/functions/deploy/deploy_test.go | 14 +++++++------- internal/functions/serve/serve.go | 23 ++--------------------- pkg/function/api.go | 2 +- pkg/function/batch.go | 4 ++-- pkg/function/batch_test.go | 10 +++++----- pkg/function/bundle.go | 19 ++++++++++++++++++- pkg/function/bundle_test.go | 2 +- 12 files changed, 59 insertions(+), 46 deletions(-) diff --git a/cmd/functions.go b/cmd/functions.go index 36dc47f94..1357b8d16 100644 --- a/cmd/functions.go +++ b/cmd/functions.go @@ -55,6 +55,7 @@ var ( noVerifyJWT = new(bool) useLegacyBundle bool importMapPath string + envFilePath string functionsDeployCmd = &cobra.Command{ Use: "deploy [Function name]", @@ -65,7 +66,7 @@ var ( if !cmd.Flags().Changed("no-verify-jwt") { noVerifyJWT = nil } - return deploy.Run(cmd.Context(), args, flags.ProjectRef, noVerifyJWT, importMapPath, afero.NewOsFs()) + return deploy.Run(cmd.Context(), args, flags.ProjectRef, noVerifyJWT, importMapPath, envFilePath, afero.NewOsFs()) }, } @@ -82,7 +83,6 @@ var ( }, } - envFilePath string inspectBrk bool inspectMode = utils.EnumFlag{ Allowed: []string{ @@ -127,6 +127,7 @@ func init() { functionsDeployCmd.Flags().StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") functionsDeployCmd.Flags().BoolVar(&useLegacyBundle, "legacy-bundle", false, "Use legacy bundling mechanism.") functionsDeployCmd.Flags().StringVar(&importMapPath, "import-map", "", "Path to import map file.") + functionsDeployCmd.Flags().StringVar(&envFilePath, "env-file", "", "Path to an env file.") cobra.CheckErr(functionsDeployCmd.Flags().MarkHidden("legacy-bundle")) functionsServeCmd.Flags().BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.") functionsServeCmd.Flags().StringVar(&envFilePath, "env-file", "", "Path to an env file to be populated to the Function environment.") diff --git a/examples/functions-deploy/main.go b/examples/functions-deploy/main.go index 716ef1462..67f1bc3e8 100644 --- a/examples/functions-deploy/main.go +++ b/examples/functions-deploy/main.go @@ -29,7 +29,7 @@ func deploy(ctx context.Context, fsys fs.FS) error { Entrypoint: "supabase/functions/my-slug/index.ts", ImportMap: "supabase/functions/import_map.json", }} - return functionClient.UpsertFunctions(ctx, fc) + return functionClient.UpsertFunctions(ctx, fc, "") } func newAPIClient(token string) api.ClientWithResponses { diff --git a/internal/functions/deploy/bundle.go b/internal/functions/deploy/bundle.go index 0119a559c..8bc09e7eb 100644 --- a/internal/functions/deploy/bundle.go +++ b/internal/functions/deploy/bundle.go @@ -25,7 +25,21 @@ func NewDockerBundler(fsys afero.Fs) function.EszipBundler { return &dockerBundler{fsys: fsys} } -func (b *dockerBundler) Bundle(ctx context.Context, entrypoint string, importMap string, output io.Writer) error { +func (b *dockerBundler) Bundle(ctx context.Context, entrypoint string, importMap string, envFilePath string, output io.Writer) error { + // read env file + if envFilePath == "" { + if f, err := b.fsys.Stat(utils.FallbackEnvFilePath); err == nil && !f.IsDir() { + envFilePath = utils.FallbackEnvFilePath + } + } else if !filepath.IsAbs(envFilePath) { + envFilePath = filepath.Join(utils.CurrentDirAbs, envFilePath) + } + // 2. Parse user defined env + env, err := function.ParseEnvFile(envFilePath, b.fsys) + if err != nil { + return err + } + // Create temp directory to store generated eszip slug := filepath.Base(filepath.Dir(entrypoint)) fmt.Fprintln(os.Stderr, "Bundling Function:", utils.Bold(slug)) @@ -62,7 +76,7 @@ func (b *dockerBundler) Bundle(ctx context.Context, entrypoint string, importMap ctx, container.Config{ Image: utils.Config.EdgeRuntime.Image, - Env: []string{}, + Env: env, Cmd: cmd, WorkingDir: utils.ToDockerPath(cwd), }, diff --git a/internal/functions/deploy/bundle_test.go b/internal/functions/deploy/bundle_test.go index f8a68f439..5207e14db 100644 --- a/internal/functions/deploy/bundle_test.go +++ b/internal/functions/deploy/bundle_test.go @@ -43,7 +43,7 @@ func TestDockerBundle(t *testing.T) { apitest.MockDockerStart(utils.Docker, imageUrl, containerId) require.NoError(t, apitest.MockDockerLogsExitCode(utils.Docker, containerId, 1)) // Run test - err = NewDockerBundler(fsys).Bundle(context.Background(), "", "", &body) + err = NewDockerBundler(fsys).Bundle(context.Background(), "", "", "", &body) // Check error assert.ErrorContains(t, err, "error running container: exit 1") assert.Empty(t, apitest.ListUnmatchedRequests()) diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go index 529b78976..b7644b7cc 100644 --- a/internal/functions/deploy/deploy.go +++ b/internal/functions/deploy/deploy.go @@ -16,7 +16,7 @@ import ( "github.com/supabase/cli/pkg/function" ) -func Run(ctx context.Context, slugs []string, projectRef string, noVerifyJWT *bool, importMapPath string, fsys afero.Fs) error { +func Run(ctx context.Context, slugs []string, projectRef string, noVerifyJWT *bool, importMapPath string, envFilePath string, fsys afero.Fs) error { // Load function config and project id if err := utils.LoadConfigFS(fsys); err != nil { return err @@ -38,7 +38,7 @@ func Run(ctx context.Context, slugs []string, projectRef string, noVerifyJWT *bo return err } api := function.NewEdgeRuntimeAPI(projectRef, *utils.GetSupabase(), NewDockerBundler(fsys)) - if err := api.UpsertFunctions(ctx, functionConfig); err != nil { + if err := api.UpsertFunctions(ctx, functionConfig, envFilePath); err != nil { return err } fmt.Printf("Deployed Functions on project %s: %s\n", utils.Aqua(projectRef), strings.Join(slugs, ", ")) diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go index 558a33f32..924831bc7 100644 --- a/internal/functions/deploy/deploy_test.go +++ b/internal/functions/deploy/deploy_test.go @@ -61,7 +61,7 @@ func TestDeployCommand(t *testing.T) { } // Run test noVerifyJWT := true - err = Run(context.Background(), functions, project, &noVerifyJWT, "", fsys) + err = Run(context.Background(), functions, project, &noVerifyJWT, "", "", fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -116,7 +116,7 @@ import_map = "./import_map.json" outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug)) require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - err = Run(context.Background(), nil, project, nil, "", fsys) + err = Run(context.Background(), nil, project, nil, "", "", fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -168,7 +168,7 @@ import_map = "./import_map.json" outputDir := filepath.Join(utils.TempDir, ".output_enabled-func") require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - err = Run(context.Background(), nil, project, nil, "", fsys) + err = Run(context.Background(), nil, project, nil, "", "", fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -179,7 +179,7 @@ import_map = "./import_map.json" fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) // Run test - err := Run(context.Background(), []string{"_invalid"}, "", nil, "", fsys) + err := Run(context.Background(), []string{"_invalid"}, "", nil, "", "", fsys) // Check error assert.ErrorContains(t, err, "Invalid Function name.") }) @@ -189,7 +189,7 @@ import_map = "./import_map.json" fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) // Run test - err := Run(context.Background(), nil, "", nil, "", fsys) + err := Run(context.Background(), nil, "", nil, "", "", fsys) // Check error assert.ErrorContains(t, err, "No Functions specified or found in supabase/functions") }) @@ -234,7 +234,7 @@ verify_jwt = false outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug)) require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - assert.NoError(t, Run(context.Background(), []string{slug}, project, nil, "", fsys)) + assert.NoError(t, Run(context.Background(), []string{slug}, project, nil, "", "", fsys)) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) @@ -280,7 +280,7 @@ verify_jwt = false require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test noVerifyJwt := false - assert.NoError(t, Run(context.Background(), []string{slug}, project, &noVerifyJwt, "", fsys)) + assert.NoError(t, Run(context.Background(), []string{slug}, project, &noVerifyJwt, "", "", fsys)) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go index 62811d769..5fd128fd3 100644 --- a/internal/functions/serve/serve.go +++ b/internal/functions/serve/serve.go @@ -17,8 +17,8 @@ import ( "github.com/spf13/afero" "github.com/spf13/viper" "github.com/supabase/cli/internal/functions/deploy" - "github.com/supabase/cli/internal/secrets/set" "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/pkg/function" ) type InspectMode string @@ -105,7 +105,7 @@ func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool, envFilePath = filepath.Join(utils.CurrentDirAbs, envFilePath) } // 2. Parse user defined env - env, err := parseEnvFile(envFilePath, fsys) + env, err := function.ParseEnvFile(envFilePath, fsys) if err != nil { return err } @@ -187,25 +187,6 @@ EOF return err } -func parseEnvFile(envFilePath string, fsys afero.Fs) ([]string, error) { - env := []string{} - if len(envFilePath) == 0 { - return env, nil - } - envMap, err := set.ParseEnvFile(envFilePath, fsys) - if err != nil { - return env, err - } - for name, value := range envMap { - if strings.HasPrefix(name, "SUPABASE_") { - fmt.Fprintln(os.Stderr, "Env name cannot start with SUPABASE_, skipping: "+name) - continue - } - env = append(env, name+"="+value) - } - return env, nil -} - func populatePerFunctionConfigs(cwd, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) ([]string, string, error) { slugs, err := deploy.GetFunctionSlugs(fsys) if err != nil { diff --git a/pkg/function/api.go b/pkg/function/api.go index e3f641dee..4ddfd23e6 100644 --- a/pkg/function/api.go +++ b/pkg/function/api.go @@ -14,7 +14,7 @@ type EdgeRuntimeAPI struct { } type EszipBundler interface { - Bundle(ctx context.Context, entrypoint string, importMap string, output io.Writer) error + Bundle(ctx context.Context, entrypoint string, importMap string, envFilePath string, output io.Writer) error } func NewEdgeRuntimeAPI(project string, client api.ClientWithResponses, bundler EszipBundler) EdgeRuntimeAPI { diff --git a/pkg/function/batch.go b/pkg/function/batch.go index 43f837fc4..3b3bf089e 100644 --- a/pkg/function/batch.go +++ b/pkg/function/batch.go @@ -21,7 +21,7 @@ const ( maxRetries = 3 ) -func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig config.FunctionConfig, filter ...func(string) bool) error { +func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig config.FunctionConfig, envFilePath string, filter ...func(string) bool) error { var result []api.FunctionResponse if resp, err := s.client.V1ListAllFunctionsWithResponse(ctx, s.project); err != nil { return errors.Errorf("failed to list functions: %w", err) @@ -45,7 +45,7 @@ func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig con } } var body bytes.Buffer - if err := s.eszip.Bundle(ctx, function.Entrypoint, function.ImportMap, &body); err != nil { + if err := s.eszip.Bundle(ctx, function.Entrypoint, function.ImportMap, envFilePath, &body); err != nil { return err } // Update if function already exists diff --git a/pkg/function/batch_test.go b/pkg/function/batch_test.go index 1489cd329..ee1f9a841 100644 --- a/pkg/function/batch_test.go +++ b/pkg/function/batch_test.go @@ -17,7 +17,7 @@ import ( type MockBundler struct { } -func (b *MockBundler) Bundle(ctx context.Context, entrypoint string, importMap string, output io.Writer) error { +func (b *MockBundler) Bundle(ctx context.Context, entrypoint string, importMap string, envFilePath string, output io.Writer) error { return nil } @@ -38,7 +38,7 @@ func TestUpsertFunctions(t *testing.T) { Get("/v1/projects/" + mockProject + "/functions"). ReplyError(errors.New("network error")) // Run test - err := client.UpsertFunctions(context.Background(), nil) + err := client.UpsertFunctions(context.Background(), nil, "") // Check error assert.ErrorContains(t, err, "network error") }) @@ -50,7 +50,7 @@ func TestUpsertFunctions(t *testing.T) { Get("/v1/projects/" + mockProject + "/functions"). Reply(http.StatusServiceUnavailable) // Run test - err := client.UpsertFunctions(context.Background(), nil) + err := client.UpsertFunctions(context.Background(), nil, "") // Check error assert.ErrorContains(t, err, "unexpected status 503:") }) @@ -75,7 +75,7 @@ func TestUpsertFunctions(t *testing.T) { // Run test err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ "test": {}, - }) + }, "") // Check error assert.NoError(t, err) }) @@ -100,7 +100,7 @@ func TestUpsertFunctions(t *testing.T) { // Run test err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ "test": {}, - }) + }, "") // Check error assert.NoError(t, err) }) diff --git a/pkg/function/bundle.go b/pkg/function/bundle.go index ed12806e8..fb667ad51 100644 --- a/pkg/function/bundle.go +++ b/pkg/function/bundle.go @@ -11,6 +11,8 @@ import ( "github.com/andybalholm/brotli" "github.com/go-errors/errors" + "github.com/spf13/afero" + "github.com/supabase/cli/internal/utils" ) type nativeBundler struct { @@ -28,7 +30,21 @@ func NewNativeBundler(tempDir string, fsys fs.FS) EszipBundler { // Use a package private variable to allow testing without gosec complaining about G204 var edgeRuntimeBin = "edge-runtime" -func (b *nativeBundler) Bundle(ctx context.Context, entrypoint string, importMap string, output io.Writer) error { +func (b *nativeBundler) Bundle(ctx context.Context, entrypoint string, importMap string, envFilePath string, output io.Writer) error { + // read env file + if envFilePath == "" { + if f, err := fs.Stat(b.fsys, utils.FallbackEnvFilePath); err == nil && !f.IsDir() { + envFilePath = utils.FallbackEnvFilePath + } + } else if !filepath.IsAbs(envFilePath) { + envFilePath = filepath.Join(utils.CurrentDirAbs, envFilePath) + } + // 2. Parse user defined env + env, err := ParseEnvFile(envFilePath, afero.FromIOFS{b.fsys}) + if err != nil { + return err + } + slug := filepath.Base(filepath.Dir(entrypoint)) outputPath := filepath.Join(b.tempDir, slug+".eszip") // TODO: make edge runtime write to stdout @@ -39,6 +55,7 @@ func (b *nativeBundler) Bundle(ctx context.Context, entrypoint string, importMap cmd := exec.CommandContext(ctx, edgeRuntimeBin, args...) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout + cmd.Env = env if err := cmd.Run(); err != nil { return errors.Errorf("failed to bundle function: %w", err) } diff --git a/pkg/function/bundle_test.go b/pkg/function/bundle_test.go index 1aa63f212..e8bfc7350 100644 --- a/pkg/function/bundle_test.go +++ b/pkg/function/bundle_test.go @@ -36,7 +36,7 @@ func TestBundleFunction(t *testing.T) { // Setup mock bundler bundler := nativeBundler{fsys: fsys} // Run test - err := bundler.Bundle(context.Background(), "hello/index.ts", "", &body) + err := bundler.Bundle(context.Background(), "hello/index.ts", "", "", &body) // Check error assert.NoError(t, err) assert.Equal(t, compressedEszipMagicID+";", body.String()) From 1771afdb3ddf6770da6dbb603f6b1a0d513ecbec Mon Sep 17 00:00:00 2001 From: Lakshan Perera Date: Tue, 24 Dec 2024 15:49:42 +1100 Subject: [PATCH 2/2] update example --- examples/functions-deploy/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/functions-deploy/main.go b/examples/functions-deploy/main.go index 67f1bc3e8..5e7f2515b 100644 --- a/examples/functions-deploy/main.go +++ b/examples/functions-deploy/main.go @@ -29,7 +29,8 @@ func deploy(ctx context.Context, fsys fs.FS) error { Entrypoint: "supabase/functions/my-slug/index.ts", ImportMap: "supabase/functions/import_map.json", }} - return functionClient.UpsertFunctions(ctx, fc, "") + envFilePath := "" + return functionClient.UpsertFunctions(ctx, fc, envFilePath) } func newAPIClient(token string) api.ClientWithResponses {