github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/functions/serve/serve.go (about)

     1  package serve
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/Redstoneguy129/cli/internal/utils"
    13  	"github.com/docker/docker/api/types"
    14  	"github.com/docker/docker/api/types/container"
    15  	"github.com/docker/docker/pkg/stdcopy"
    16  	"github.com/joho/godotenv"
    17  	"github.com/spf13/afero"
    18  	"github.com/spf13/viper"
    19  )
    20  
    21  const (
    22  	relayFuncDir              = "/home/deno/functions"
    23  	customDockerImportMapPath = "/home/deno/import_map.json"
    24  )
    25  
    26  func ParseEnvFile(envFilePath string) ([]string, error) {
    27  	env := []string{}
    28  	if len(envFilePath) == 0 {
    29  		return env, nil
    30  	}
    31  	envMap, err := godotenv.Read(envFilePath)
    32  	if err != nil {
    33  		return env, err
    34  	}
    35  	for name, value := range envMap {
    36  		if strings.HasPrefix(name, "SUPABASE_") {
    37  			return env, errors.New("Invalid env name: " + name + ". Env names cannot start with SUPABASE_.")
    38  		}
    39  		env = append(env, name+"="+value)
    40  	}
    41  	return env, nil
    42  }
    43  
    44  func Run(ctx context.Context, slug string, envFilePath string, noVerifyJWT *bool, importMapPath string, serveAll bool, fsys afero.Fs) error {
    45  	if serveAll {
    46  		return runServeAll(ctx, envFilePath, noVerifyJWT, importMapPath, fsys)
    47  	}
    48  
    49  	// 1. Sanity checks.
    50  	{
    51  		if err := utils.LoadConfigFS(fsys); err != nil {
    52  			return err
    53  		}
    54  		if err := utils.AssertSupabaseDbIsRunning(); err != nil {
    55  			return err
    56  		}
    57  		if err := utils.ValidateFunctionSlug(slug); err != nil {
    58  			return err
    59  		}
    60  		if envFilePath != "" {
    61  			if _, err := fsys.Stat(envFilePath); err != nil {
    62  				return fmt.Errorf("Failed to read env file: %w", err)
    63  			}
    64  		}
    65  		if importMapPath != "" {
    66  			// skip
    67  		} else if functionConfig, ok := utils.Config.Functions[slug]; ok && functionConfig.ImportMap != "" {
    68  			if filepath.IsAbs(functionConfig.ImportMap) {
    69  				importMapPath = functionConfig.ImportMap
    70  			} else {
    71  				importMapPath = filepath.Join(utils.SupabaseDirPath, functionConfig.ImportMap)
    72  			}
    73  		} else if f, err := fsys.Stat(utils.FallbackImportMapPath); err == nil && !f.IsDir() {
    74  			importMapPath = utils.FallbackImportMapPath
    75  		}
    76  		if importMapPath != "" {
    77  			if _, err := fsys.Stat(importMapPath); err != nil {
    78  				return fmt.Errorf("Failed to read import map: %w", err)
    79  			}
    80  		}
    81  	}
    82  
    83  	// 2. Parse user defined env
    84  	userEnv, err := ParseEnvFile(envFilePath)
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	// 3. Start relay.
    90  	{
    91  		_ = utils.Docker.ContainerRemove(ctx, utils.DenoRelayId, types.ContainerRemoveOptions{
    92  			RemoveVolumes: true,
    93  			Force:         true,
    94  		})
    95  
    96  		env := []string{
    97  			"JWT_SECRET=" + utils.JWTSecret,
    98  			"DENO_ORIGIN=http://" + utils.Config.Hostname + ":8000",
    99  		}
   100  		verifyJWTEnv := "VERIFY_JWT=true"
   101  		if noVerifyJWT == nil {
   102  			if functionConfig, ok := utils.Config.Functions[slug]; ok && !*functionConfig.VerifyJWT {
   103  				verifyJWTEnv = "VERIFY_JWT=false"
   104  			}
   105  		} else if *noVerifyJWT {
   106  			verifyJWTEnv = "VERIFY_JWT=false"
   107  		}
   108  		env = append(env, verifyJWTEnv)
   109  
   110  		cwd, err := os.Getwd()
   111  		if err != nil {
   112  			return err
   113  		}
   114  
   115  		binds := []string{filepath.Join(cwd, utils.FunctionsDir) + ":" + relayFuncDir + ":ro,z"}
   116  		// If a import map path is explcitly provided, mount it as a separate file
   117  		if importMapPath != "" {
   118  			binds = append(binds, filepath.Join(cwd, importMapPath)+":"+customDockerImportMapPath+":ro,z")
   119  		}
   120  		if _, err := utils.DockerStart(
   121  			ctx,
   122  			container.Config{
   123  				Image: utils.DenoRelayImage,
   124  				Env:   append(env, userEnv...),
   125  			},
   126  			container.HostConfig{
   127  				Binds: binds,
   128  				// Allows containerized functions on Linux to reach host OS
   129  				ExtraHosts: []string{"host.docker.internal:host-gateway"},
   130  			},
   131  			utils.DenoRelayId,
   132  		); err != nil {
   133  			return err
   134  		}
   135  
   136  		go func() {
   137  			<-ctx.Done()
   138  			if ctx.Err() != nil {
   139  				utils.DockerRemove(utils.DenoRelayId)
   140  			}
   141  		}()
   142  	}
   143  
   144  	// 4. Start Function.
   145  	localFuncDir := filepath.Join(utils.FunctionsDir, slug)
   146  	localImportMapPath := filepath.Join(localFuncDir, "import_map.json")
   147  
   148  	// We assume the image is always Linux, so path separator must always be `/`.
   149  	// We can't use filepath.Join because it uses the path separator for the host system, which is `\` for Windows.
   150  	dockerFuncPath := relayFuncDir + "/" + slug + "/index.ts"
   151  	dockerImportMapPath := relayFuncDir + "/" + slug + "/import_map.json"
   152  
   153  	if importMapPath != "" {
   154  		localImportMapPath = importMapPath
   155  		dockerImportMapPath = customDockerImportMapPath
   156  	}
   157  
   158  	denoCacheCmd := []string{"deno", "cache"}
   159  	{
   160  		if _, err := fsys.Stat(localImportMapPath); err == nil {
   161  			denoCacheCmd = append(denoCacheCmd, "--import-map="+dockerImportMapPath)
   162  		} else if errors.Is(err, os.ErrNotExist) {
   163  			// skip
   164  		} else {
   165  			return fmt.Errorf("failed to check import_map.json for function %s: %w", slug, err)
   166  		}
   167  		denoCacheCmd = append(denoCacheCmd, dockerFuncPath)
   168  	}
   169  
   170  	fmt.Println("Starting " + utils.Bold(localFuncDir))
   171  	if _, err := utils.DockerExecOnce(ctx, utils.DenoRelayId, userEnv, denoCacheCmd); err != nil {
   172  		return err
   173  	}
   174  
   175  	{
   176  		fmt.Println("Serving " + utils.Bold(localFuncDir))
   177  
   178  		env := []string{
   179  			"SUPABASE_URL=http://" + utils.KongId + ":8000",
   180  			"SUPABASE_ANON_KEY=" + utils.AnonKey,
   181  			"SUPABASE_SERVICE_ROLE_KEY=" + utils.ServiceRoleKey,
   182  			"SUPABASE_DB_URL=postgresql://postgres:postgres@" + utils.Config.Hostname + ":" + strconv.FormatUint(uint64(utils.Config.Db.Port), 10) + "/postgres",
   183  		}
   184  
   185  		denoRunCmd := []string{"deno", "run", "--no-check=remote", "--allow-all", "--watch", "--no-clear-screen", "--no-npm"}
   186  		{
   187  			if _, err := fsys.Stat(localImportMapPath); err == nil {
   188  				denoRunCmd = append(denoRunCmd, "--import-map="+dockerImportMapPath)
   189  			} else if errors.Is(err, os.ErrNotExist) {
   190  				// skip
   191  			} else {
   192  				return fmt.Errorf("failed to check index.ts for function %s: %w", slug, err)
   193  			}
   194  			denoRunCmd = append(denoRunCmd, dockerFuncPath)
   195  		}
   196  
   197  		exec, err := utils.Docker.ContainerExecCreate(
   198  			ctx,
   199  			utils.DenoRelayId,
   200  			types.ExecConfig{
   201  				Env:          append(env, userEnv...),
   202  				Cmd:          denoRunCmd,
   203  				AttachStderr: true,
   204  				AttachStdout: true,
   205  			},
   206  		)
   207  		if err != nil {
   208  			return err
   209  		}
   210  
   211  		resp, err := utils.Docker.ContainerExecAttach(ctx, exec.ID, types.ExecStartCheck{})
   212  		if err != nil {
   213  			return err
   214  		}
   215  
   216  		if _, err := stdcopy.StdCopy(os.Stdout, os.Stderr, resp.Reader); err != nil {
   217  			return err
   218  		}
   219  	}
   220  
   221  	fmt.Println("Stopped serving " + utils.Bold(localFuncDir))
   222  	return nil
   223  }
   224  
   225  func runServeAll(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, fsys afero.Fs) error {
   226  	// 1. Sanity checks.
   227  	{
   228  		if err := utils.LoadConfigFS(fsys); err != nil {
   229  			return err
   230  		}
   231  		if err := utils.AssertSupabaseDbIsRunning(); err != nil {
   232  			return err
   233  		}
   234  		if envFilePath != "" {
   235  			if _, err := fsys.Stat(envFilePath); err != nil {
   236  				return fmt.Errorf("Failed to read env file: %w", err)
   237  			}
   238  		}
   239  		if importMapPath != "" {
   240  			// skip
   241  		} else if f, err := fsys.Stat(utils.FallbackImportMapPath); err == nil && !f.IsDir() {
   242  			importMapPath = utils.FallbackImportMapPath
   243  		}
   244  		if importMapPath != "" {
   245  			if _, err := fsys.Stat(importMapPath); err != nil {
   246  				return fmt.Errorf("Failed to read import map: %w", err)
   247  			}
   248  		}
   249  	}
   250  
   251  	// 2. Parse user defined env
   252  	userEnv, err := ParseEnvFile(envFilePath)
   253  	if err != nil {
   254  		return err
   255  	}
   256  
   257  	// 3. Start container
   258  	{
   259  		_ = utils.Docker.ContainerRemove(ctx, utils.DenoRelayId, types.ContainerRemoveOptions{
   260  			RemoveVolumes: true,
   261  			Force:         true,
   262  		})
   263  
   264  		env := []string{
   265  			"JWT_SECRET=" + utils.JWTSecret,
   266  			"SUPABASE_URL=http://" + utils.KongId + ":8000",
   267  			"SUPABASE_ANON_KEY=" + utils.AnonKey,
   268  			"SUPABASE_SERVICE_ROLE_KEY=" + utils.ServiceRoleKey,
   269  			"SUPABASE_DB_URL=postgresql://postgres:postgres@" + utils.Config.Hostname + ":" + strconv.FormatUint(uint64(utils.Config.Db.Port), 10) + "/postgres",
   270  		}
   271  		verifyJWTEnv := "VERIFY_JWT=true"
   272  		if noVerifyJWT != nil {
   273  			verifyJWTEnv = "VERIFY_JWT=false"
   274  		}
   275  		env = append(env, verifyJWTEnv)
   276  
   277  		cwd, err := os.Getwd()
   278  		if err != nil {
   279  			return err
   280  		}
   281  
   282  		binds := []string{
   283  			filepath.Join(cwd, utils.FunctionsDir) + ":" + relayFuncDir + ":ro,z",
   284  			utils.DenoRelayId + ":/root/.cache/deno:rw,z",
   285  		}
   286  		// If a import map path is explcitly provided, mount it as a separate file
   287  		if importMapPath != "" {
   288  			binds = append(binds, filepath.Join(cwd, importMapPath)+":"+customDockerImportMapPath+":ro,z")
   289  		}
   290  
   291  		fmt.Println("Serving " + utils.Bold(utils.FunctionsDir))
   292  
   293  		cmd := []string{"start", "--dir", relayFuncDir, "-p", "8081"}
   294  		if viper.GetBool("DEBUG") {
   295  			cmd = append(cmd, "--verbose")
   296  		}
   297  		if err := utils.DockerRunOnceWithStream(
   298  			ctx,
   299  			utils.EdgeRuntimeImage,
   300  			append(env, userEnv...),
   301  			cmd,
   302  			binds,
   303  			utils.DenoRelayId,
   304  			os.Stdout,
   305  			os.Stderr,
   306  		); err != nil {
   307  			return err
   308  		}
   309  	}
   310  
   311  	fmt.Println("Stopped serving " + utils.Bold(utils.FunctionsDir))
   312  	return nil
   313  
   314  }