github.com/supabase/cli@v1.168.1/internal/functions/serve/serve.go (about)

     1  package serve
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/docker/docker/api/types/container"
    14  	"github.com/docker/docker/api/types/network"
    15  	"github.com/docker/go-connections/nat"
    16  	"github.com/go-errors/errors"
    17  	"github.com/spf13/afero"
    18  	"github.com/spf13/viper"
    19  	"github.com/supabase/cli/internal/db/start"
    20  	"github.com/supabase/cli/internal/secrets/set"
    21  	"github.com/supabase/cli/internal/utils"
    22  )
    23  
    24  const (
    25  	// Import Map from CLI flag, i.e. --import-map, takes priority over config.toml & fallback.
    26  	dockerFlagImportMapPath     = utils.DockerDenoDir + "/flag_import_map.json"
    27  	dockerFallbackImportMapPath = utils.DockerDenoDir + "/fallback_import_map.json"
    28  )
    29  
    30  var (
    31  	//go:embed templates/main.ts
    32  	mainFuncEmbed string
    33  )
    34  
    35  func Run(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, fsys afero.Fs) error {
    36  	// 1. Sanity checks.
    37  	if err := utils.LoadConfigFS(fsys); err != nil {
    38  		return err
    39  	}
    40  	if err := utils.AssertSupabaseDbIsRunning(); err != nil {
    41  		return err
    42  	}
    43  	// 2. Remove existing container.
    44  	_ = utils.Docker.ContainerRemove(ctx, utils.EdgeRuntimeId, container.RemoveOptions{
    45  		RemoveVolumes: true,
    46  		Force:         true,
    47  	})
    48  	// Use network alias because Deno cannot resolve `_` in hostname
    49  	dbUrl := "postgresql://postgres:postgres@" + utils.DbAliases[0] + ":5432/postgres"
    50  	// 3. Serve and log to console
    51  	if err := ServeFunctions(ctx, envFilePath, noVerifyJWT, importMapPath, dbUrl, os.Stderr, fsys); err != nil {
    52  		return err
    53  	}
    54  	if err := utils.DockerStreamLogs(ctx, utils.EdgeRuntimeId, os.Stdout, os.Stderr); err != nil {
    55  		return err
    56  	}
    57  	fmt.Println("Stopped serving " + utils.Bold(utils.FunctionsDir))
    58  	return nil
    59  }
    60  
    61  func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, dbUrl string, w io.Writer, fsys afero.Fs) error {
    62  	// 1. Load default values
    63  	if envFilePath == "" {
    64  		if f, err := fsys.Stat(utils.FallbackEnvFilePath); err == nil && !f.IsDir() {
    65  			envFilePath = utils.FallbackEnvFilePath
    66  		}
    67  	} else if _, err := fsys.Stat(envFilePath); err != nil {
    68  		return errors.Errorf("Failed to read env file: %w", err)
    69  	}
    70  	cwd, err := os.Getwd()
    71  	if err != nil {
    72  		return errors.Errorf("failed to get working directory: %w", err)
    73  	}
    74  	if importMapPath != "" {
    75  		if !filepath.IsAbs(importMapPath) {
    76  			importMapPath = filepath.Join(cwd, importMapPath)
    77  		}
    78  		if _, err := fsys.Stat(importMapPath); err != nil {
    79  			return errors.Errorf("Failed to read import map: %w", err)
    80  		}
    81  	}
    82  	// 2. Parse user defined env
    83  	userEnv, err := parseEnvFile(envFilePath, fsys)
    84  	if err != nil {
    85  		return err
    86  	}
    87  	env := []string{
    88  		"SUPABASE_URL=http://" + utils.KongAliases[0] + ":8000",
    89  		"SUPABASE_ANON_KEY=" + utils.Config.Auth.AnonKey,
    90  		"SUPABASE_SERVICE_ROLE_KEY=" + utils.Config.Auth.ServiceRoleKey,
    91  		"SUPABASE_DB_URL=" + dbUrl,
    92  		"SUPABASE_INTERNAL_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
    93  		fmt.Sprintf("SUPABASE_INTERNAL_HOST_PORT=%d", utils.Config.Api.Port),
    94  		"SUPABASE_INTERNAL_FUNCTIONS_PATH=" + utils.DockerFuncDirPath,
    95  	}
    96  	if viper.GetBool("DEBUG") {
    97  		env = append(env, "SUPABASE_INTERNAL_DEBUG=true")
    98  	}
    99  	// 3. Parse custom import map
   100  	binds := []string{
   101  		// Reuse deno cache directory, ie. DENO_DIR, between container restarts
   102  		// https://denolib.gitbook.io/guide/advanced/deno_dir-code-fetch-and-cache
   103  		utils.EdgeRuntimeId + ":/root/.cache/deno:rw,z",
   104  		filepath.Join(cwd, utils.FunctionsDir) + ":" + utils.DockerFuncDirPath + ":rw,z",
   105  	}
   106  	if importMapPath != "" {
   107  		modules, err := utils.BindImportMap(importMapPath, dockerFlagImportMapPath, fsys)
   108  		if err != nil {
   109  			return err
   110  		}
   111  		binds = append(binds, modules...)
   112  	}
   113  
   114  	fallbackImportMapPath := filepath.Join(cwd, utils.FallbackImportMapPath)
   115  	if exists, err := afero.Exists(fsys, fallbackImportMapPath); err != nil {
   116  		return errors.Errorf("Failed to read fallback import map: %w", err)
   117  	} else if !exists {
   118  		fallbackImportMapPath = utils.AbsTempImportMapPath(cwd, utils.ImportMapsDir)
   119  		if err := utils.WriteFile(fallbackImportMapPath, []byte(`{"imports":{}}`), fsys); err != nil {
   120  			return err
   121  		}
   122  	}
   123  	if fallbackImportMapPath != importMapPath {
   124  		modules, err := utils.BindImportMap(fallbackImportMapPath, dockerFallbackImportMapPath, fsys)
   125  		if err != nil {
   126  			return err
   127  		}
   128  		binds = append(binds, modules...)
   129  	}
   130  
   131  	if err := utils.MkdirIfNotExistFS(fsys, utils.FunctionsDir); err != nil {
   132  		return err
   133  	}
   134  	binds, functionsConfigString, err := populatePerFunctionConfigs(binds, importMapPath, noVerifyJWT, fsys)
   135  	if err != nil {
   136  		return err
   137  	}
   138  	env = append(env, "SUPABASE_INTERNAL_FUNCTIONS_CONFIG="+functionsConfigString)
   139  
   140  	// 4. Start container
   141  	fmt.Fprintln(w, "Setting up Edge Functions runtime...")
   142  
   143  	var cmdString string
   144  	{
   145  		cmd := []string{"edge-runtime", "start", "--main-service", "/home/deno/main", "-p", "8081"}
   146  		if viper.GetBool("DEBUG") {
   147  			cmd = append(cmd, "--verbose")
   148  		}
   149  		cmdString = strings.Join(cmd, " ")
   150  	}
   151  
   152  	entrypoint := []string{"sh", "-c", `mkdir -p /home/deno/main && cat <<'EOF' > /home/deno/main/index.ts && ` + cmdString + `
   153  ` + mainFuncEmbed + `
   154  EOF
   155  `}
   156  	_, err = utils.DockerStart(
   157  		ctx,
   158  		container.Config{
   159  			Image:        utils.EdgeRuntimeImage,
   160  			Env:          append(env, userEnv...),
   161  			Entrypoint:   entrypoint,
   162  			ExposedPorts: nat.PortSet{"8081/tcp": {}},
   163  			// No tcp health check because edge runtime logs them as client connection error
   164  		},
   165  		start.WithSyslogConfig(container.HostConfig{
   166  			Binds:      binds,
   167  			ExtraHosts: []string{"host.docker.internal:host-gateway"},
   168  		}),
   169  		network.NetworkingConfig{
   170  			EndpointsConfig: map[string]*network.EndpointSettings{
   171  				utils.NetId: {
   172  					Aliases: utils.EdgeRuntimeAliases,
   173  				},
   174  			},
   175  		},
   176  		utils.EdgeRuntimeId,
   177  	)
   178  	return err
   179  }
   180  
   181  func parseEnvFile(envFilePath string, fsys afero.Fs) ([]string, error) {
   182  	env := []string{}
   183  	if len(envFilePath) == 0 {
   184  		return env, nil
   185  	}
   186  	envMap, err := set.ParseEnvFile(envFilePath, fsys)
   187  	if err != nil {
   188  		return env, err
   189  	}
   190  	for name, value := range envMap {
   191  		if strings.HasPrefix(name, "SUPABASE_") {
   192  			fmt.Fprintln(os.Stderr, "Env name cannot start with SUPABASE_, skipping: "+name)
   193  			continue
   194  		}
   195  		env = append(env, name+"="+value)
   196  	}
   197  	return env, nil
   198  }
   199  
   200  func populatePerFunctionConfigs(binds []string, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) ([]string, string, error) {
   201  	type functionConfig struct {
   202  		ImportMapPath string `json:"importMapPath"`
   203  		VerifyJWT     bool   `json:"verifyJWT"`
   204  	}
   205  
   206  	functionsConfig := map[string]functionConfig{}
   207  
   208  	cwd, err := os.Getwd()
   209  	if err != nil {
   210  		return nil, "", errors.Errorf("failed to get working directory: %w", err)
   211  	}
   212  
   213  	functions, err := afero.ReadDir(fsys, utils.FunctionsDir)
   214  	if err != nil {
   215  		return nil, "", errors.Errorf("failed to read directory: %w", err)
   216  	}
   217  	for _, function := range functions {
   218  		if !function.IsDir() {
   219  			continue
   220  		}
   221  
   222  		functionName := function.Name()
   223  		if !utils.FuncSlugPattern.MatchString(functionName) {
   224  			continue
   225  		}
   226  
   227  		// CLI flags take priority over config.toml.
   228  
   229  		dockerImportMapPath := dockerFallbackImportMapPath
   230  		if importMapPath != "" {
   231  			dockerImportMapPath = dockerFlagImportMapPath
   232  		} else if functionConfig, ok := utils.Config.Functions[functionName]; ok && functionConfig.ImportMap != "" {
   233  			dockerImportMapPath = "/home/deno/import_maps/" + functionName + "/import_map.json"
   234  			hostImportMapPath := filepath.Join(cwd, utils.SupabaseDirPath, functionConfig.ImportMap)
   235  			modules, err := utils.BindImportMap(hostImportMapPath, dockerImportMapPath, fsys)
   236  			if err != nil {
   237  				return nil, "", err
   238  			}
   239  			binds = append(binds, modules...)
   240  		}
   241  
   242  		verifyJWT := true
   243  		if noVerifyJWT != nil {
   244  			verifyJWT = !*noVerifyJWT
   245  		} else if functionConfig, ok := utils.Config.Functions[functionName]; ok && functionConfig.VerifyJWT != nil {
   246  			verifyJWT = *functionConfig.VerifyJWT
   247  		}
   248  
   249  		functionsConfig[functionName] = functionConfig{
   250  			ImportMapPath: dockerImportMapPath,
   251  			VerifyJWT:     verifyJWT,
   252  		}
   253  	}
   254  
   255  	functionsConfigBytes, err := json.Marshal(functionsConfig)
   256  	if err != nil {
   257  		return nil, "", errors.Errorf("failed to marshal config json: %w", err)
   258  	}
   259  
   260  	return binds, string(functionsConfigBytes), nil
   261  }