From 5f93ecd17cbb29d5b2d85f92fd114d26698ca774 Mon Sep 17 00:00:00 2001 From: "Alex Ellis (OpenFaaS Ltd)" Date: Fri, 30 Jun 2023 15:48:32 +0100 Subject: [PATCH 1/5] Implement --watch for localrun with cancellation support This commit adds --watch support to localrun by using a context to cancel and remove any running container when a change is detected. watchLoop has been updated to support a context. Tested with VSCode - I suggest turning auto-save off when using this feature. Tested again with faas-cli up to make sure there was no regression: https://asciinema.org/a/594072 Signed-off-by: Alex Ellis (OpenFaaS Ltd) --- commands/build.go | 7 +- commands/local_run.go | 78 +++++++++++-- commands/up.go | 202 ++-------------------------------- commands/watch.go | 248 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 325 insertions(+), 210 deletions(-) create mode 100644 commands/watch.go diff --git a/commands/build.go b/commands/build.go index b37c56107..e71a241c0 100644 --- a/commands/build.go +++ b/commands/build.go @@ -192,7 +192,7 @@ func runBuild(cmd *cobra.Command, args []string) error { return fmt.Errorf("please provide the deployed --name of your function") } - err := builder.BuildImage(image, + if err := builder.BuildImage(image, handler, functionName, language, @@ -207,10 +207,10 @@ func runBuild(cmd *cobra.Command, args []string) error { copyExtra, remoteBuilder, payloadSecretPath, - ) - if err != nil { + ); err != nil { return err } + return nil } @@ -222,6 +222,7 @@ func runBuild(cmd *cobra.Command, args []string) error { } return fmt.Errorf("%s", aec.Apply(errorSummary, aec.RedF)) } + return nil } diff --git a/commands/local_run.go b/commands/local_run.go index a9d96b5d3..024636d7f 100644 --- a/commands/local_run.go +++ b/commands/local_run.go @@ -4,16 +4,20 @@ import ( "context" "fmt" "io" + "log" "os" "path/filepath" "strings" + "syscall" "os/exec" + "os/signal" "github.com/openfaas/faas-cli/builder" "github.com/openfaas/faas-cli/schema" "github.com/openfaas/faas-cli/stack" "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" ) const localSecretsDir = ".secrets" @@ -78,7 +82,7 @@ services deployed within your OpenFaaS cluster.`, cmd.Flags().StringVar(&opts.network, "network", "", "connect function to an existing network, use 'host' to access other process already running on localhost. When using this, '--port' is ignored, if you have port collisions, you may change the port using '-e port=NEW_PORT'") cmd.Flags().StringToStringVarP(&opts.extraEnv, "env", "e", map[string]string{}, "additional environment variables (ENVVAR=VALUE), use this to experiment with different values for your function") - cmd.Flags().Bool("watch", false, "Watch for changes in handler code and rebuild") + cmd.Flags().BoolVar(&watch, "watch", false, "Watch for changes in files and re-deploy") build, _, _ := faasCmd.Find([]string{"build"}) cmd.Flags().AddFlagSet(build.Flags()) @@ -98,18 +102,19 @@ func runLocalRunE(cmd *cobra.Command, args []string) error { return watchLoop(cmd, args, localRunExec) } - return localRunExec(cmd, args) + ctx, cancel := context.WithCancel(cmd.Context()) + defer cancel() + + return localRunExec(cmd, args, ctx) } -func localRunExec(cmd *cobra.Command, args []string) error { +func localRunExec(cmd *cobra.Command, args []string, ctx context.Context) error { if opts.build { if err := localBuild(cmd, args); err != nil { return err } } - ctx := cmd.Context() - opts.output = cmd.OutOrStdout() opts.err = cmd.ErrOrStderr() @@ -187,7 +192,10 @@ func runFunction(ctx context.Context, name string, opts runOptions, args []strin } } - cmd, err := buildDockerRun(ctx, services.Functions[name], opts) + // Always try to remove before running, to clear up any previous state + removeContainer(name) + + cmd, err := buildDockerRun(ctx, name, services.Functions[name], opts) if err != nil { return err } @@ -197,21 +205,67 @@ func runFunction(ctx context.Context, name string, opts runOptions, args []strin return nil } + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + cmd.Stdout = opts.output cmd.Stderr = opts.err fmt.Printf("Starting local-run for: %s on: http://0.0.0.0:%d\n\n", name, opts.port) + grpContext := context.Background() + grpContext, cancel := context.WithCancel(grpContext) + defer cancel() - if err = cmd.Start(); err != nil { - return err - } + errGrp, _ := errgroup.WithContext(grpContext) + + errGrp.Go(func() error { + if err = cmd.Start(); err != nil { + return err + } + + if err := cmd.Wait(); err != nil { + if strings.Contains(err.Error(), "signal: killed") { + return nil + } else if strings.Contains(err.Error(), "os: process already finished") { + return nil + } + + return err + } + return nil + }) + + // Always try to remove the container + defer func() { + removeContainer(name) + }() + + errGrp.Go(func() error { + + select { + case <-sigs: + log.Printf("Caught signal, exiting") + cancel() + case <-ctx.Done(): + log.Printf("Context cancelled, exiting..") + cancel() + } + return nil + }) + + return errGrp.Wait() +} + +func removeContainer(name string) { + + runDockerRm := exec.Command("docker", "rm", "-f", name) + runDockerRm.Run() - return cmd.Wait() } // buildDockerRun constructs a exec.Cmd from the given stack Function -func buildDockerRun(ctx context.Context, fnc stack.Function, opts runOptions) (*exec.Cmd, error) { - args := []string{"run", "--rm", "-i", fmt.Sprintf("-p=%d:8080", opts.port)} +func buildDockerRun(ctx context.Context, name string, fnc stack.Function, opts runOptions) (*exec.Cmd, error) { + args := []string{"run", "--name", name, "--rm", "-i", fmt.Sprintf("-p=%d:8080", opts.port)} if opts.network != "" { args = append(args, fmt.Sprintf("--network=%s", opts.network)) diff --git a/commands/up.go b/commands/up.go index 67e4de1a3..49e1d3c0e 100644 --- a/commands/up.go +++ b/commands/up.go @@ -5,20 +5,12 @@ package commands import ( "bufio" + "context" "fmt" - "log" "os" - "os/signal" - "path" - "path/filepath" "strings" - "syscall" - "time" - "github.com/bep/debounce" - "github.com/fsnotify/fsnotify" "github.com/go-git/go-git/v5/plumbing/format/gitignore" - "github.com/openfaas/faas-cli/stack" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -82,197 +74,17 @@ func preRunUp(cmd *cobra.Command, args []string) error { } func upHandler(cmd *cobra.Command, args []string) error { - return watchLoop(cmd, args, func(cmd *cobra.Command, args []string) error { - return upRunner(cmd, args) - }) -} - -func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Command, args []string) error) error { - - // Always run an initial build to freshen up - if err := onChange(cmd, args); err != nil { - return err - } - - var services stack.Services - if len(yamlFile) > 0 { - parsedServices, err := stack.ParseYAMLFile(yamlFile, regex, filter, envsubst) - if err != nil { - return err - } - - if parsedServices != nil { - services = *parsedServices - } - } - - fnNames := []string{} - for name, _ := range services.Functions { - fnNames = append(fnNames, name) - } - - fmt.Printf("[Watch] monitoring %d functions: %s\n", len(fnNames), strings.Join(fnNames, ", ")) - if watch { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - defer watcher.Close() - - patterns, err := ignorePatterns() - if err != nil { - return err - } - - matcher := gitignore.NewMatcher(patterns) - - cwd, err := os.Getwd() - if err != nil { - return err - } - yamlPath := path.Join(cwd, yamlFile) - - debug := os.Getenv("FAAS_DEBUG") - if debug == "1" { - fmt.Printf("[Watch] added: %s\n", yamlPath) - } - watcher.Add(yamlPath) - - // map to determine which function belongs to changed files - // when responding to events - handlerMap := make(map[string]string) - - for serviceName, service := range services.Functions { - handlerMap[serviceName] = path.Join(cwd, service.Handler) - - handlerFullPath := path.Join(cwd, service.Handler) - - if err := addPath(watcher, handlerFullPath); err != nil { - return err - } - } - - signalChannel := make(chan os.Signal, 1) - // Exit on Ctrl+C or kill - signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) - - d := debounce.New(2 * time.Second) - - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return fmt.Errorf("watcher's Events channel is closed") - } - log.Printf("%s %s", event.Op, event.Name) - - if strings.HasSuffix(event.Name, ".swp") || strings.HasSuffix(event.Name, "~") || strings.HasSuffix(event.Name, ".swx") { - continue - } - - if event.Op == fsnotify.Write || event.Op == fsnotify.Create || event.Op == fsnotify.Remove || event.Op == fsnotify.Rename { - - info, err := os.Stat(event.Name) - if err != nil { - continue - } - ignore := false - if matcher.Match(strings.Split(event.Name, "/"), info.IsDir()) { - ignore = true - } - - // exact match first - target := "" - for fnName, fnPath := range handlerMap { - if event.Name == fnPath { - target = fnName - } - } - - // fuzzy match after, if none matched exactly - if target == "" { - for fnName, fnPath := range handlerMap { - log.Printf("Checking %s against %s", event.Name, fnPath) - - if strings.HasPrefix(event.Name, fnPath) { - target = fnName - } - } - } - - // New sub-directory added for a function, start tracking it - if event.Op == fsnotify.Create && info.IsDir() && target != "" { - if err := addPath(watcher, event.Name); err != nil { - return err - } - } - - if !ignore { - if target == "" { - fmt.Printf("Rebuilding %d functions reason: %s to %s\n", len(fnNames), event.Op, event.Name) - } else { - fmt.Printf("Reloading %s reason: %s to %s\n", target, event.Op, event.Name) - } - } - - if !ignore { - d(func() { - filter = target - - go func() { - // Assign --filter to "" for all functions if we can't determine the - // changed function to direct the calls to build/push/deploy - - if err := onChange(cmd, args); err != nil { - fmt.Println("Error rebuilding: ", err) - os.Exit(1) - } - }() - }) - } - - } - - case err, ok := <-watcher.Errors: - if !ok { - return fmt.Errorf("watcher's Errors channel is closed") - } - return err - - case <-signalChannel: - watcher.Close() - return nil - } - } + return watchLoop(cmd, args, func(cmd *cobra.Command, args []string, ctx context.Context) error { + return upRunner(cmd, args, ctx) + }) } - return nil -} - -func addPath(watcher *fsnotify.Watcher, rootPath string) error { - debug := os.Getenv("FAAS_DEBUG") - - return filepath.WalkDir(rootPath, func(subPath string, d os.DirEntry, err error) error { - if err != nil { - return err - } - - if d.IsDir() { - if err := watcher.Add(subPath); err != nil { - return fmt.Errorf("unable to watch %s: %s", subPath, err) - } - - if debug == "1" { - fmt.Printf("[Watch] added: %s\n", subPath) - } - } - - return nil - }) + ctx := context.Background() + return upRunner(cmd, args, ctx) } -func upRunner(cmd *cobra.Command, args []string) error { +func upRunner(cmd *cobra.Command, args []string, ctx context.Context) error { if err := runBuild(cmd, args); err != nil { return err } diff --git a/commands/watch.go b/commands/watch.go new file mode 100644 index 000000000..813af36f5 --- /dev/null +++ b/commands/watch.go @@ -0,0 +1,248 @@ +package commands + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "path" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/bep/debounce" + "github.com/fsnotify/fsnotify" + "github.com/go-git/go-git/v5/plumbing/format/gitignore" + "github.com/openfaas/faas-cli/stack" + "github.com/spf13/cobra" +) + +// watchLoop will watch for changes to function handler files and the stack.yml +// then call onChange when a change is detected +func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Command, args []string, ctx context.Context) error) error { + + var services stack.Services + if len(yamlFile) > 0 { + parsedServices, err := stack.ParseYAMLFile(yamlFile, regex, filter, envsubst) + if err != nil { + return err + } + + if parsedServices != nil { + services = *parsedServices + } + } + + fnNames := []string{} + for name, _ := range services.Functions { + fnNames = append(fnNames, name) + } + + fmt.Printf("[Watch] monitoring %d functions: %s\n", len(fnNames), strings.Join(fnNames, ", ")) + + canceller := Cancel{} + + if watch { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + defer watcher.Close() + + patterns, err := ignorePatterns() + if err != nil { + return err + } + + matcher := gitignore.NewMatcher(patterns) + + cwd, err := os.Getwd() + if err != nil { + return err + } + yamlPath := path.Join(cwd, yamlFile) + + debug := os.Getenv("FAAS_DEBUG") + + if debug == "1" { + fmt.Printf("[Watch] added: %s\n", yamlPath) + } + + watcher.Add(yamlPath) + + // map to determine which function belongs to changed files + // when responding to events + handlerMap := make(map[string]string) + + for serviceName, service := range services.Functions { + handlerMap[serviceName] = path.Join(cwd, service.Handler) + + handlerFullPath := path.Join(cwd, service.Handler) + + if err := addPath(watcher, handlerFullPath); err != nil { + return err + } + } + + signalChannel := make(chan os.Signal, 1) + + // Exit on Ctrl+C or kill + signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) + + bounce := debounce.New(1500 * time.Second) + + var cancel context.CancelFunc + var ctx context.Context + + // An initial build is usually done on first load with + // live reloaders + go bounce(func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + canceller.Set(ctx, cancel) + + if err := onChange(cmd, args, ctx); err != nil { + fmt.Println("Error rebuilding: ", err) + os.Exit(1) + } + }) + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return fmt.Errorf("watcher's Events channel is closed") + } + + if debug == "1" { + log.Printf("[Watch] event: %s on: %s", event.Op, event.Name) + } + if strings.HasSuffix(event.Name, ".swp") || strings.HasSuffix(event.Name, "~") || strings.HasSuffix(event.Name, ".swx") { + continue + } + + if event.Op == fsnotify.Write || event.Op == fsnotify.Create || event.Op == fsnotify.Remove || event.Op == fsnotify.Rename { + + info, err := os.Stat(event.Name) + if err != nil { + continue + } + ignore := false + if matcher.Match(strings.Split(event.Name, "/"), info.IsDir()) { + ignore = true + } + + // exact match first + target := "" + for fnName, fnPath := range handlerMap { + if event.Name == fnPath { + target = fnName + } + } + + // fuzzy match after, if none matched exactly + if target == "" { + for fnName, fnPath := range handlerMap { + + if strings.HasPrefix(event.Name, fnPath) { + target = fnName + } + } + } + + // New sub-directory added for a function, start tracking it + if event.Op == fsnotify.Create && info.IsDir() && target != "" { + if err := addPath(watcher, event.Name); err != nil { + return err + } + } + + if !ignore { + if target == "" { + fmt.Printf("[Watch] Rebuilding %d functions reason: %s to %s\n", len(fnNames), event.Op, event.Name) + } else { + fmt.Printf("[Watch] Reloading %s reason: %s to %s\n", target, event.Op, event.Name) + } + } + + if !ignore { + bounce(func() { + + canceller.Cancel() + + ctx, cancel = context.WithCancel(context.Background()) + canceller.Set(ctx, cancel) + + // Assign --filter to "" for all functions if we can't determine the + // changed function to direct the calls to build/push/deploy + filter = target + + go func() { + if err := onChange(cmd, args, ctx); err != nil { + fmt.Println("Error rebuilding: ", err) + os.Exit(1) + } + }() + }) + } + + } + + case err, ok := <-watcher.Errors: + if !ok { + return fmt.Errorf("watcher's Errors channel is closed") + } + return err + + case <-signalChannel: + watcher.Close() + return nil + } + } + } + return nil +} + +func addPath(watcher *fsnotify.Watcher, rootPath string) error { + debug := os.Getenv("FAAS_DEBUG") + + return filepath.WalkDir(rootPath, func(subPath string, d os.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + if err := watcher.Add(subPath); err != nil { + return fmt.Errorf("unable to watch %s: %s", subPath, err) + } + + if debug == "1" { + fmt.Printf("[Watch] added: %s\n", subPath) + } + } + + return nil + }) + +} + +// Cancel is a struct to hold a reference to a context and +// cancellation function between closures +type Cancel struct { + cancel context.CancelFunc + ctx context.Context +} + +func (c *Cancel) Set(ctx context.Context, cancel context.CancelFunc) { + c.cancel = cancel + c.ctx = ctx +} + +func (c *Cancel) Cancel() { + if c.cancel != nil { + c.cancel() + } + +} From d230460862596112ae7601af778fb0f5c3c0a774 Mon Sep 17 00:00:00 2001 From: "Alex Ellis (OpenFaaS Ltd)" Date: Fri, 30 Jun 2023 16:10:30 +0100 Subject: [PATCH 2/5] Fix debounce from 1500s to 1500ms Signed-off-by: Alex Ellis (OpenFaaS Ltd) --- commands/up.go | 8 +- commands/watch.go | 223 ++++++++++++++++++++++------------------------ 2 files changed, 111 insertions(+), 120 deletions(-) diff --git a/commands/up.go b/commands/up.go index 49e1d3c0e..9dbe44282 100644 --- a/commands/up.go +++ b/commands/up.go @@ -76,6 +76,9 @@ func preRunUp(cmd *cobra.Command, args []string) error { func upHandler(cmd *cobra.Command, args []string) error { if watch { return watchLoop(cmd, args, func(cmd *cobra.Command, args []string, ctx context.Context) error { + + fmt.Println("[Watch] Change a file to trigger a rebuild...") + return upRunner(cmd, args, ctx) }) } @@ -99,11 +102,6 @@ func upRunner(cmd *cobra.Command, args []string, ctx context.Context) error { if err := runDeploy(cmd, args); err != nil { return err } - - if watch { - fmt.Println("[Watch] Change a file to trigger a rebuild...") - } - } return nil diff --git a/commands/watch.go b/commands/watch.go index 813af36f5..a6ab122a2 100644 --- a/commands/watch.go +++ b/commands/watch.go @@ -44,164 +44,157 @@ func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Comma canceller := Cancel{} - if watch { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - defer watcher.Close() + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + defer watcher.Close() - patterns, err := ignorePatterns() - if err != nil { - return err - } + patterns, err := ignorePatterns() + if err != nil { + return err + } - matcher := gitignore.NewMatcher(patterns) + matcher := gitignore.NewMatcher(patterns) - cwd, err := os.Getwd() - if err != nil { - return err - } - yamlPath := path.Join(cwd, yamlFile) + cwd, err := os.Getwd() + if err != nil { + return err + } + yamlPath := path.Join(cwd, yamlFile) - debug := os.Getenv("FAAS_DEBUG") + debug := os.Getenv("FAAS_DEBUG") - if debug == "1" { - fmt.Printf("[Watch] added: %s\n", yamlPath) - } + if debug == "1" { + fmt.Printf("[Watch] added: %s\n", yamlPath) + } - watcher.Add(yamlPath) + watcher.Add(yamlPath) - // map to determine which function belongs to changed files - // when responding to events - handlerMap := make(map[string]string) + // map to determine which function belongs to changed files + // when responding to events + handlerMap := make(map[string]string) - for serviceName, service := range services.Functions { - handlerMap[serviceName] = path.Join(cwd, service.Handler) + for serviceName, service := range services.Functions { + handlerMap[serviceName] = path.Join(cwd, service.Handler) - handlerFullPath := path.Join(cwd, service.Handler) + handlerFullPath := path.Join(cwd, service.Handler) - if err := addPath(watcher, handlerFullPath); err != nil { - return err - } + if err := addPath(watcher, handlerFullPath); err != nil { + return err } + } + + signalChannel := make(chan os.Signal, 1) - signalChannel := make(chan os.Signal, 1) + // Exit on Ctrl+C or kill + signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) - // Exit on Ctrl+C or kill - signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) + bounce := debounce.New(1500 * time.Millisecond) - bounce := debounce.New(1500 * time.Second) + // An initial build is usually done on first load with + // live reloaders + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + canceller.Set(ctx, cancel) - var cancel context.CancelFunc - var ctx context.Context + if err := onChange(cmd, args, ctx); err != nil { + fmt.Println("Error rebuilding: ", err) + os.Exit(1) + } - // An initial build is usually done on first load with - // live reloaders - go bounce(func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - canceller.Set(ctx, cancel) + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return fmt.Errorf("watcher's Events channel is closed") + } - if err := onChange(cmd, args, ctx); err != nil { - fmt.Println("Error rebuilding: ", err) - os.Exit(1) + if debug == "1" { + log.Printf("[Watch] event: %s on: %s", strings.ToLower(event.Op.String()), event.Name) + } + if strings.HasSuffix(event.Name, ".swp") || strings.HasSuffix(event.Name, "~") || strings.HasSuffix(event.Name, ".swx") { + continue } - }) - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return fmt.Errorf("watcher's Events channel is closed") - } + if event.Op == fsnotify.Write || event.Op == fsnotify.Create || event.Op == fsnotify.Remove || event.Op == fsnotify.Rename { - if debug == "1" { - log.Printf("[Watch] event: %s on: %s", event.Op, event.Name) - } - if strings.HasSuffix(event.Name, ".swp") || strings.HasSuffix(event.Name, "~") || strings.HasSuffix(event.Name, ".swx") { + info, err := os.Stat(event.Name) + if err != nil { continue } + ignore := false + if matcher.Match(strings.Split(event.Name, "/"), info.IsDir()) { + ignore = true + } - if event.Op == fsnotify.Write || event.Op == fsnotify.Create || event.Op == fsnotify.Remove || event.Op == fsnotify.Rename { - - info, err := os.Stat(event.Name) - if err != nil { - continue - } - ignore := false - if matcher.Match(strings.Split(event.Name, "/"), info.IsDir()) { - ignore = true + // exact match first + target := "" + for fnName, fnPath := range handlerMap { + if event.Name == fnPath { + target = fnName } + } - // exact match first - target := "" + // fuzzy match after, if none matched exactly + if target == "" { for fnName, fnPath := range handlerMap { - if event.Name == fnPath { - target = fnName - } - } - // fuzzy match after, if none matched exactly - if target == "" { - for fnName, fnPath := range handlerMap { - - if strings.HasPrefix(event.Name, fnPath) { - target = fnName - } + if strings.HasPrefix(event.Name, fnPath) { + target = fnName } } + } - // New sub-directory added for a function, start tracking it - if event.Op == fsnotify.Create && info.IsDir() && target != "" { - if err := addPath(watcher, event.Name); err != nil { - return err - } + // New sub-directory added for a function, start tracking it + if event.Op == fsnotify.Create && info.IsDir() && target != "" { + if err := addPath(watcher, event.Name); err != nil { + return err } + } - if !ignore { - if target == "" { - fmt.Printf("[Watch] Rebuilding %d functions reason: %s to %s\n", len(fnNames), event.Op, event.Name) - } else { - fmt.Printf("[Watch] Reloading %s reason: %s to %s\n", target, event.Op, event.Name) - } + if !ignore { + if target == "" { + fmt.Printf("[Watch] Rebuilding %d functions reason: %s to %s\n", len(fnNames), strings.ToLower(event.Op.String()), event.Name) + } else { + fmt.Printf("[Watch] Reloading %s reason: %s %s\n", target, strings.ToLower(event.Op.String()), event.Name) } - if !ignore { - bounce(func() { + bounce(func() { + log.Printf("[Watch] Cancel previous build") - canceller.Cancel() + canceller.Cancel() + log.Printf("[Watch] Cancelled previous build") + ctx, cancel = context.WithCancel(context.Background()) + canceller.Set(ctx, cancel) - ctx, cancel = context.WithCancel(context.Background()) - canceller.Set(ctx, cancel) - - // Assign --filter to "" for all functions if we can't determine the - // changed function to direct the calls to build/push/deploy - filter = target - - go func() { - if err := onChange(cmd, args, ctx); err != nil { - fmt.Println("Error rebuilding: ", err) - os.Exit(1) - } - }() - }) - } + // Assign --filter to "" for all functions if we can't determine the + // changed function to direct the calls to build/push/deploy + filter = target + go func() { + if err := onChange(cmd, args, ctx); err != nil { + fmt.Println("Error rebuilding: ", err) + os.Exit(1) + } + }() + }) } - case err, ok := <-watcher.Errors: - if !ok { - return fmt.Errorf("watcher's Errors channel is closed") - } - return err + } - case <-signalChannel: - watcher.Close() - return nil + case err, ok := <-watcher.Errors: + if !ok { + return fmt.Errorf("watcher's Errors channel is closed") } + return err + + case <-signalChannel: + watcher.Close() + return nil } } + return nil } From bd295b0cfdcdb41885d9da3980865d653bd4cf61 Mon Sep 17 00:00:00 2001 From: "Alex Ellis (OpenFaaS Ltd)" Date: Fri, 30 Jun 2023 16:17:57 +0100 Subject: [PATCH 3/5] Move reload message for up command This change moves the reload message to where it can be seen instead of having it printed out mid-flow during the up sequence. Signed-off-by: Alex Ellis (OpenFaaS Ltd) --- commands/up.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/commands/up.go b/commands/up.go index 9dbe44282..df14fe96b 100644 --- a/commands/up.go +++ b/commands/up.go @@ -77,9 +77,11 @@ func upHandler(cmd *cobra.Command, args []string) error { if watch { return watchLoop(cmd, args, func(cmd *cobra.Command, args []string, ctx context.Context) error { + if err := upRunner(cmd, args, ctx); err != nil { + return err + } fmt.Println("[Watch] Change a file to trigger a rebuild...") - - return upRunner(cmd, args, ctx) + return nil }) } From a4895028b7ed33d3ca5c63af8f0ea5c6d1dfe0c3 Mon Sep 17 00:00:00 2001 From: "Alex Ellis (OpenFaaS Ltd)" Date: Fri, 30 Jun 2023 16:44:42 +0100 Subject: [PATCH 4/5] Run initial command in a go routine Signed-off-by: Alex Ellis (OpenFaaS Ltd) --- commands/watch.go | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/commands/watch.go b/commands/watch.go index a6ab122a2..80012af12 100644 --- a/commands/watch.go +++ b/commands/watch.go @@ -92,17 +92,20 @@ func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Comma bounce := debounce.New(1500 * time.Millisecond) - // An initial build is usually done on first load with - // live reloaders - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - canceller.Set(ctx, cancel) - - if err := onChange(cmd, args, ctx); err != nil { - fmt.Println("Error rebuilding: ", err) - os.Exit(1) - } + go func() { + // An initial build is usually done on first load with + // live reloaders + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + canceller.Set(ctx, cancel) + + if err := onChange(cmd, args, ctx); err != nil { + fmt.Println("Error rebuilding: ", err) + os.Exit(1) + } + }() + log.Printf("[Watch] Started") for { select { case event, ok := <-watcher.Events: @@ -161,11 +164,12 @@ func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Comma } bounce(func() { - log.Printf("[Watch] Cancel previous build") + log.Printf("[Watch] Cancelling") canceller.Cancel() - log.Printf("[Watch] Cancelled previous build") - ctx, cancel = context.WithCancel(context.Background()) + + log.Printf("[Watch] Cancelled") + ctx, cancel := context.WithCancel(context.Background()) canceller.Set(ctx, cancel) // Assign --filter to "" for all functions if we can't determine the From 0f8ccb8668a6901bbf5230342164fd8393e718fb Mon Sep 17 00:00:00 2001 From: "Alex Ellis (OpenFaaS Ltd)" Date: Fri, 30 Jun 2023 17:00:11 +0100 Subject: [PATCH 5/5] Enable switching out to publish + deploy Publish and deploy is quicker than build push and deploy because it doesn't run two commands and doesn't put data into the local library first. Signed-off-by: Alex Ellis (OpenFaaS Ltd) --- commands/up.go | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/commands/up.go b/commands/up.go index df14fe96b..6c4544bd7 100644 --- a/commands/up.go +++ b/commands/up.go @@ -18,12 +18,15 @@ import ( var ( skipPush bool skipDeploy bool + usePublish bool watch bool ) func init() { upFlagset := pflag.NewFlagSet("up", pflag.ExitOnError) + upFlagset.BoolVar(&usePublish, "publish", false, "Use faas-cli publish instead of faas-cli build followed by faas-cli push") + upFlagset.BoolVar(&skipPush, "skip-push", false, "Skip pushing function to remote registry") upFlagset.BoolVar(&skipDeploy, "skip-deploy", false, "Skip function deployment") upFlagset.StringVar(&remoteBuilder, "remote-builder", "", "URL to the builder") @@ -57,8 +60,19 @@ and the deploy step with --skip-deploy. Note: All flags from the build, push and deploy flags are valid and can be combined, see the --help text for those commands for details.`, - Example: ` faas-cli up -f myfn.yaml -faas-cli up --filter "*gif*" --secret dockerhuborg`, + Example: ` # Deploy everything + faas-cli up + + # Deploy a named function + faas-cli up --filter echo + + # Deploy but skip the push step + faas-cli up --skip-push + + # Build but skip pushing and use a build-arg + faas-cli up --skip-push \ + --build-arg GO111MODULE=on + `, PreRunE: preRunUp, RunE: upHandler, } @@ -90,14 +104,20 @@ func upHandler(cmd *cobra.Command, args []string) error { } func upRunner(cmd *cobra.Command, args []string, ctx context.Context) error { - if err := runBuild(cmd, args); err != nil { - return err - } - - if !skipPush && remoteBuilder == "" { - if err := runPush(cmd, args); err != nil { + if usePublish { + if err := runPublish(cmd, args); err != nil { return err } + } else { + if err := runBuild(cmd, args); err != nil { + return err + } + + if !skipPush && remoteBuilder == "" { + if err := runPush(cmd, args); err != nil { + return err + } + } } if !skipDeploy {