From 4339b62aafe3b02d67cb4896552077283b7d895a Mon Sep 17 00:00:00 2001 From: Lucas Roesler Date: Sat, 1 Jul 2023 18:33:51 +0200 Subject: [PATCH] feat: use ctx propogation to enable docker build cancellation Copy the execute package into the project to add the require context passing. This can be pulled back later once we are happy with the implementation. Streamline some of the watch and local-run code to use the native Context object instead of the Cancel object. Signed-off-by: Lucas Roesler --- builder/build.go | 17 ++-- commands/build.go | 30 ++++--- commands/faas.go | 7 +- commands/local_run.go | 83 +++++++----------- commands/up.go | 10 +-- commands/watch.go | 196 ++++++++++++++++++++---------------------- contexts/signals.go | 32 +++++++ execute/exec.go | 144 +++++++++++++++++++++++++++++++ logger/logging.go | 36 ++++++++ proxy/logs.go | 6 +- 10 files changed, 378 insertions(+), 183 deletions(-) create mode 100644 contexts/signals.go create mode 100644 execute/exec.go create mode 100644 logger/logging.go diff --git a/builder/build.go b/builder/build.go index 9d26b954..8087ef5a 100644 --- a/builder/build.go +++ b/builder/build.go @@ -4,9 +4,11 @@ package builder import ( + "context" "crypto/md5" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -18,7 +20,7 @@ import ( "sort" "strings" - v1execute "github.com/alexellis/go-execute/pkg/v1" + "github.com/openfaas/faas-cli/execute" "github.com/openfaas/faas-cli/schema" "github.com/openfaas/faas-cli/stack" vcs "github.com/openfaas/faas-cli/versioncontrol" @@ -30,7 +32,7 @@ const AdditionalPackageBuildArg = "ADDITIONAL_PACKAGE" // BuildImage construct Docker image from function parameters // TODO: refactor signature to a struct to simplify the length of the method header -func BuildImage(image string, handler string, functionName string, language string, nocache bool, squash bool, shrinkwrap bool, buildArgMap map[string]string, buildOptions []string, tagFormat schema.BuildFormat, buildLabelMap map[string]string, quietBuild bool, copyExtraPaths []string, remoteBuilder, payloadSecretPath string) error { +func BuildImage(ctx context.Context, image string, handler string, functionName string, language string, nocache bool, squash bool, shrinkwrap bool, buildArgMap map[string]string, buildOptions []string, tagFormat schema.BuildFormat, buildLabelMap map[string]string, quietBuild bool, copyExtraPaths []string, remoteBuilder, payloadSecretPath string) error { if stack.IsValidTemplate(language) { pathToTemplateYAML := fmt.Sprintf("./template/%s/template.yml", language) @@ -135,7 +137,7 @@ func BuildImage(image string, handler string, functionName string, language stri envs = append(envs, "DOCKER_BUILDKIT=1") } - task := v1execute.ExecTask{ + task := execute.ExecTask{ Cwd: tempPath, Command: command, Args: args, @@ -143,14 +145,17 @@ func BuildImage(image string, handler string, functionName string, language stri Env: envs, } - res, err := task.Execute() - + res, err := task.Execute(ctx) if err != nil { return err } + if res.ExitCode == -1 && errors.Is(ctx.Err(), context.Canceled) { + return ctx.Err() + } + if res.ExitCode != 0 { - return fmt.Errorf("[%s] received non-zero exit code from build, error: %s", functionName, res.Stderr) + return fmt.Errorf("[%s] received non-zero exit code %d from build, error: %s", functionName, res.ExitCode, res.Stderr) } fmt.Printf("Image: %s built.\n", imageName) diff --git a/commands/build.go b/commands/build.go index e71a241c..a1114832 100644 --- a/commands/build.go +++ b/commands/build.go @@ -4,6 +4,8 @@ package commands import ( + "context" + "errors" "fmt" "log" "os" @@ -151,7 +153,7 @@ func parseBuildArgs(args []string) (map[string]string, error) { } func runBuild(cmd *cobra.Command, args []string) error { - + ctx := cmd.Context() var services stack.Services if len(yamlFile) > 0 { parsedServices, err := stack.ParseYAMLFile(yamlFile, regex, filter, envsubst) @@ -192,7 +194,9 @@ func runBuild(cmd *cobra.Command, args []string) error { return fmt.Errorf("please provide the deployed --name of your function") } - if err := builder.BuildImage(image, + if err := builder.BuildImage( + ctx, + image, handler, functionName, language, @@ -214,7 +218,12 @@ func runBuild(cmd *cobra.Command, args []string) error { return nil } - errors := build(&services, parallel, shrinkwrap, quietBuild) + // a proper multi-error such as https://github.com/hashicorp/go-multierror + // would be nice here. In the current implementation we are unable to inspect + // the cause of the error, so we opted to just hide context cancel errors, + // but this makes it harder to detect this case upstream in the call stack. + + errors := build(ctx, &services, parallel, shrinkwrap, quietBuild) if len(errors) > 0 { errorSummary := "Errors received during build:\n" for _, err := range errors { @@ -226,10 +235,10 @@ func runBuild(cmd *cobra.Command, args []string) error { return nil } -func build(services *stack.Services, queueDepth int, shrinkwrap, quietBuild bool) []error { +func build(ctx context.Context, services *stack.Services, queueDepth int, shrinkwrap, quietBuild bool) []error { startOuter := time.Now() - errors := []error{} + buildErrors := []error{} wg := sync.WaitGroup{} @@ -248,7 +257,9 @@ func build(services *stack.Services, queueDepth int, shrinkwrap, quietBuild bool combinedBuildOptions := combineBuildOpts(function.BuildOptions, buildOptions) combinedBuildArgMap := util.MergeMap(function.BuildArgs, buildArgMap) combinedExtraPaths := util.MergeSlice(services.StackConfiguration.CopyExtraPaths, copyExtra) - err := builder.BuildImage(function.Image, + err := builder.BuildImage( + ctx, + function.Image, function.Handler, function.Name, function.Language, @@ -264,9 +275,8 @@ func build(services *stack.Services, queueDepth int, shrinkwrap, quietBuild bool remoteBuilder, payloadSecretPath, ) - - if err != nil { - errors = append(errors, err) + if err != nil && !errors.Is(err, context.Canceled) { + buildErrors = append(buildErrors, err) } } @@ -295,7 +305,7 @@ func build(services *stack.Services, queueDepth int, shrinkwrap, quietBuild bool duration := time.Since(startOuter) fmt.Printf("\n%s\n", aec.Apply(fmt.Sprintf("Total build time: %1.2fs", duration.Seconds()), aec.YellowF)) - return errors + return buildErrors } // pullTemplates pulls templates from specified git remote. templateURL may be a pinned repository. diff --git a/commands/faas.go b/commands/faas.go index c888b090..ad2b5af3 100644 --- a/commands/faas.go +++ b/commands/faas.go @@ -4,6 +4,7 @@ package commands import ( + "context" "fmt" "log" "os" @@ -12,6 +13,7 @@ import ( "syscall" "github.com/moby/term" + "github.com/openfaas/faas-cli/contexts" "github.com/openfaas/faas-cli/version" "github.com/spf13/cobra" ) @@ -74,6 +76,9 @@ func init() { func Execute(customArgs []string) { checkAndSetDefaultYaml() + ctx, cancel := contexts.WithSignals(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + faasCmd.SilenceUsage = true faasCmd.SilenceErrors = true faasCmd.SetArgs(customArgs[1:]) @@ -105,7 +110,7 @@ func Execute(customArgs []string) { } } - if err := faasCmd.Execute(); err != nil { + if err := faasCmd.ExecuteContext(ctx); err != nil { e := err.Error() fmt.Println(strings.ToUpper(e[:1]) + e[1:]) os.Exit(1) diff --git a/commands/local_run.go b/commands/local_run.go index 024636d7..a18ce441 100644 --- a/commands/local_run.go +++ b/commands/local_run.go @@ -8,16 +8,14 @@ import ( "os" "path/filepath" "strings" - "syscall" "os/exec" - "os/signal" "github.com/openfaas/faas-cli/builder" + "github.com/openfaas/faas-cli/logger" "github.com/openfaas/faas-cli/schema" "github.com/openfaas/faas-cli/stack" "github.com/spf13/cobra" - "golang.org/x/sync/errgroup" ) const localSecretsDir = ".secrets" @@ -102,15 +100,20 @@ func runLocalRunE(cmd *cobra.Command, args []string) error { return watchLoop(cmd, args, localRunExec) } - ctx, cancel := context.WithCancel(cmd.Context()) - defer cancel() - - return localRunExec(cmd, args, ctx) + return localRunExec(cmd, args) } -func localRunExec(cmd *cobra.Command, args []string, ctx context.Context) error { +func localRunExec(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() if opts.build { + log.Printf("[Local-Run] Building function") if err := localBuild(cmd, args); err != nil { + if err == context.Canceled { + log.Printf("[Local-Run] Context cancelled, build cancelled") + return nil + } + + logger.Debugf("[Local-Run] Error building function: %s", err.Error()) return err } } @@ -123,6 +126,13 @@ func localRunExec(cmd *cobra.Command, args []string, ctx context.Context) error name = args[0] } + // In watch mode, it is possible that runFunction might be invoked after a cancelled build. + if ctx.Err() != nil { + log.Printf("[Local-Run] Context cancelled, skipping run") + return nil + } + + logger.Debugf("[Local-Run] Starting execution: %s", name) return runFunction(ctx, name, opts, args) } @@ -204,63 +214,32 @@ func runFunction(ctx context.Context, name string, opts runOptions, args []strin fmt.Fprintf(opts.output, "%s\n", cmd.String()) 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() - - 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 - } + // Always try to remove the container + defer removeContainer(name) - return err - } - return nil - }) + if err = cmd.Start(); err != nil { + return err + } - // 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() + 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 nil - }) - return errGrp.Wait() + return err + } + return nil } func removeContainer(name string) { - runDockerRm := exec.Command("docker", "rm", "-f", name) runDockerRm.Run() - } // buildDockerRun constructs a exec.Cmd from the given stack Function diff --git a/commands/up.go b/commands/up.go index 6c4544bd..7e4da5a3 100644 --- a/commands/up.go +++ b/commands/up.go @@ -5,7 +5,6 @@ package commands import ( "bufio" - "context" "fmt" "os" "strings" @@ -89,9 +88,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 { + return watchLoop(cmd, args, func(cmd *cobra.Command, args []string) error { - if err := upRunner(cmd, args, ctx); err != nil { + if err := upRunner(cmd, args); err != nil { return err } fmt.Println("[Watch] Change a file to trigger a rebuild...") @@ -99,11 +98,10 @@ func upHandler(cmd *cobra.Command, args []string) error { }) } - ctx := context.Background() - return upRunner(cmd, args, ctx) + return upRunner(cmd, args) } -func upRunner(cmd *cobra.Command, args []string, ctx context.Context) error { +func upRunner(cmd *cobra.Command, args []string) error { if usePublish { if err := runPublish(cmd, args); err != nil { return err diff --git a/commands/watch.go b/commands/watch.go index 80012af1..581d5c9b 100644 --- a/commands/watch.go +++ b/commands/watch.go @@ -3,25 +3,27 @@ package commands import ( "context" "fmt" + "io/fs" "log" "os" - "os/signal" "path" "path/filepath" "strings" - "syscall" + "sync" "time" "github.com/bep/debounce" "github.com/fsnotify/fsnotify" "github.com/go-git/go-git/v5/plumbing/format/gitignore" + "github.com/openfaas/faas-cli/logger" "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 { +func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Command, args []string) error) error { + mainCtx := cmd.Context() var services stack.Services if len(yamlFile) > 0 { @@ -42,8 +44,6 @@ func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Comma fmt.Printf("[Watch] monitoring %d functions: %s\n", len(fnNames), strings.Join(fnNames, ", ")) - canceller := Cancel{} - watcher, err := fsnotify.NewWatcher() if err != nil { return err @@ -63,12 +63,7 @@ func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Comma } yamlPath := path.Join(cwd, yamlFile) - debug := os.Getenv("FAAS_DEBUG") - - if debug == "1" { - fmt.Printf("[Watch] added: %s\n", yamlPath) - } - + logger.Debugf("[Watch] added: %s\n", yamlPath) watcher.Add(yamlPath) // map to determine which function belongs to changed files @@ -85,23 +80,29 @@ func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Comma } } - signalChannel := make(chan os.Signal, 1) + bounce := debounce.New(1500 * time.Millisecond) - // Exit on Ctrl+C or kill - signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) + onChangeCtx, onChangeCancel := context.WithCancel(mainCtx) + defer onChangeCancel() - bounce := debounce.New(1500 * time.Millisecond) + // the WaitGroup is used to enable the watch+debounce to easily wait for + // each onChange invocation to complete or fully cancel before starting + // the next one. Without this, because the `cmd` is a shared pointer instead + // of a value, when we changed the onChangeCtx, it would propogate to the + // currently cancelling onChange invocation. If this handler contains many + // steps, it would be possible for it continue with the new context. + // This was seen in the local-run, the build would cancel but not return, + // so it would try to run the just aborted build and produce errors. + var wg sync.WaitGroup + wg.Add(1) go func() { + defer wg.Done() // 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) + cmd.SetContext(onChangeCtx) + if err := onChange(cmd, args); err != nil { + fmt.Println("Error on initial run: ", err) } }() @@ -113,79 +114,70 @@ func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Comma return fmt.Errorf("watcher's Events channel is closed") } - 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") { + logger.Debugf("[Watch] event: %s on: %s", strings.ToLower(event.Op.String()), event.Name) + + info, trigger := shouldTrigger(event) + if !trigger { 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 } + } - // exact match first - target := "" + // fuzzy match after, if none matched exactly + if target == "" { for fnName, fnPath := range handlerMap { - if event.Name == fnPath { + + if strings.HasPrefix(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 } + } - // 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), 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) - } - - bounce(func() { - log.Printf("[Watch] Cancelling") + // now check if the file is ignored and should not trigger the onChange + if matcher.Match(strings.Split(event.Name, "/"), info.IsDir()) { + continue + } - canceller.Cancel() + 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) + } - log.Printf("[Watch] Cancelled") - ctx, cancel := context.WithCancel(context.Background()) - canceller.Set(ctx, cancel) + bounce(func() { + log.Printf("[Watch] Cancelling") + onChangeCancel() + wg.Wait() - // Assign --filter to "" for all functions if we can't determine the - // changed function to direct the calls to build/push/deploy - filter = target + log.Printf("[Watch] Cancelled") + onChangeCtx, onChangeCancel = context.WithCancel(mainCtx) + cmd.SetContext(onChangeCtx) - 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 - } + wg.Add(1) + go func() { + defer wg.Done() + if err := onChange(cmd, args); err != nil { + fmt.Println("Error on change: ", err) + } + }() + }) case err, ok := <-watcher.Errors: if !ok { @@ -193,18 +185,35 @@ func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Comma } return err - case <-signalChannel: + case <-mainCtx.Done(): watcher.Close() return nil } } +} + +// shouldTrigger returns true if the event should trigger a rebuild. This currently +// includes create, write, remove, and rename events. +func shouldTrigger(event fsnotify.Event) (fs.FileInfo, bool) { + // skip temp and swap files + if strings.HasSuffix(event.Name, ".swp") || strings.HasSuffix(event.Name, "~") || strings.HasSuffix(event.Name, ".swx") { + return nil, false + } + + // only trigger for content changes, this skips chmod, chown, etc. + if event.Has(fsnotify.Create) || event.Has(fsnotify.Write) || event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) { + info, err := os.Stat(event.Name) + if err != nil { + return nil, false + } - return nil + return info, true + } + + return nil, false } 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 @@ -215,31 +224,10 @@ func addPath(watcher *fsnotify.Watcher, rootPath string) error { return fmt.Errorf("unable to watch %s: %s", subPath, err) } - if debug == "1" { - fmt.Printf("[Watch] added: %s\n", subPath) - } + logger.Debugf("[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() - } - -} diff --git a/contexts/signals.go b/contexts/signals.go new file mode 100644 index 00000000..c619ece0 --- /dev/null +++ b/contexts/signals.go @@ -0,0 +1,32 @@ +package contexts + +import ( + "context" + "log" + "os" + "os/signal" +) + +// WithSignals returns a context that is canceled when the process receives one of the given signals. +// When ctx is nil, a default Background context is used. +// When signals is empty, the context will be canceled by the default os.Interrupt signal. +func WithSignals(ctx context.Context, signals ...os.Signal) (context.Context, context.CancelFunc) { + if ctx == nil { + ctx = context.Background() + } + + if len(signals) == 0 { + signals = []os.Signal{os.Interrupt} + } + + ctx, cancel := context.WithCancel(ctx) + c := make(chan os.Signal, 1) + signal.Notify(c, signals...) + go func() { + sig := <-c + log.Printf("Received signal: %q. Terminating...", sig.String()) + cancel() + }() + + return ctx, cancel +} diff --git a/execute/exec.go b/execute/exec.go new file mode 100644 index 00000000..d7fb057e --- /dev/null +++ b/execute/exec.go @@ -0,0 +1,144 @@ +package execute + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "strings" +) + +const ExitCodeCancelled = -1 + +type ExecTask struct { + Command string + Args []string + Shell bool + Env []string + Cwd string + + // Stdin connect a reader to stdin for the command + // being executed. + Stdin io.Reader + + // StreamStdio prints stdout and stderr directly to os.Stdout/err as + // the command runs. + StreamStdio bool + + // PrintCommand prints the command before executing + PrintCommand bool +} + +type ExecResult struct { + // Stdout contains the stdout content from the command + Stdout string + // Stderr contains the stderr content from the command + Stderr string + // ExitCode will be the exit code of the command, + // or -1 if the command never started or was cancelled. + ExitCode int + // Cancelled indicates if the command context was cancelled + // this can be used to interpret the ExitCode. + Cancelled bool +} + +func (et ExecTask) Execute(ctx context.Context) (ExecResult, error) { + argsSt := "" + if len(et.Args) > 0 { + argsSt = strings.Join(et.Args, " ") + } + + if et.PrintCommand { + fmt.Println("exec: ", et.Command, argsSt) + } + + var cmd *exec.Cmd + + if et.Shell { + var args []string + if len(et.Args) == 0 { + startArgs := strings.Split(et.Command, " ") + script := strings.Join(startArgs, " ") + args = append([]string{"-c"}, fmt.Sprintf("%s", script)) + + } else { + script := strings.Join(et.Args, " ") + args = append([]string{"-c"}, fmt.Sprintf("%s %s", et.Command, script)) + + } + + cmd = exec.CommandContext(ctx, "/bin/bash", args...) + } else { + if strings.Index(et.Command, " ") > 0 { + parts := strings.Split(et.Command, " ") + command := parts[0] + args := parts[1:] + cmd = exec.CommandContext(ctx, command, args...) + + } else { + cmd = exec.CommandContext(ctx, et.Command, et.Args...) + } + } + + cmd.Dir = et.Cwd + + if len(et.Env) > 0 { + overrides := map[string]bool{} + for _, env := range et.Env { + key := strings.Split(env, "=")[0] + overrides[key] = true + cmd.Env = append(cmd.Env, env) + } + + for _, env := range os.Environ() { + key := strings.Split(env, "=")[0] + + if _, ok := overrides[key]; !ok { + cmd.Env = append(cmd.Env, env) + } + } + } + if et.Stdin != nil { + cmd.Stdin = et.Stdin + } + + stdoutBuff := bytes.Buffer{} + stderrBuff := bytes.Buffer{} + + var stdoutWriters io.Writer + var stderrWriters io.Writer + + if et.StreamStdio { + stdoutWriters = io.MultiWriter(os.Stdout, &stdoutBuff) + stderrWriters = io.MultiWriter(os.Stderr, &stderrBuff) + } else { + stdoutWriters = &stdoutBuff + stderrWriters = &stderrBuff + } + + cmd.Stdout = stdoutWriters + cmd.Stderr = stderrWriters + + startErr := cmd.Start() + + if startErr != nil { + return ExecResult{}, startErr + } + + exitCode := 0 + execErr := cmd.Wait() + if execErr != nil { + if exitError, ok := execErr.(*exec.ExitError); ok { + exitCode = exitError.ExitCode() + } + } + + return ExecResult{ + Stdout: stdoutBuff.String(), + Stderr: stderrBuff.String(), + ExitCode: exitCode, + Cancelled: ctx.Err() == context.Canceled, + }, nil +} diff --git a/logger/logging.go b/logger/logging.go new file mode 100644 index 00000000..1c2445c5 --- /dev/null +++ b/logger/logging.go @@ -0,0 +1,36 @@ +// logger should immediately be replaced by https://pkg.go.dev/log/slog@master +// when the project upgrades to Go 1.21 +package logger + +import ( + "log" + "os" +) + +var debug = false + +func init() { + if os.Getenv("FAAS_DEBUG") == "1" { + debug = true + } +} + +func Debug(message string) { + if debug { + log.Println(message) + } +} + +func Debugf(format string, v ...interface{}) { + if debug { + log.Printf(format, v...) + } +} + +func Print(message string) { + log.Println(message) +} + +func Printf(format string, v ...interface{}) { + log.Printf(format, v...) +} diff --git a/proxy/logs.go b/proxy/logs.go index 17305122..a7b00109 100644 --- a/proxy/logs.go +++ b/proxy/logs.go @@ -9,10 +9,10 @@ import ( "log" "net/http" "net/url" - "os" "strconv" "time" + "github.com/openfaas/faas-cli/logger" "github.com/openfaas/faas-provider/logs" ) @@ -26,9 +26,7 @@ func (c *Client) GetLogs(ctx context.Context, params logs.Request) (<-chan logs. logRequest.URL.RawQuery = reqAsQueryValues(params).Encode() - if os.Getenv("FAAS_DEBUG") == "1" { - fmt.Printf("%s\n", logRequest.URL.RawQuery) - } + logger.Debugf("%s\n", logRequest.URL.RawQuery) res, err := c.doRequest(ctx, logRequest) if err != nil {