github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/worker/exec/common.go (about)

     1  package exec
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"encoding/json"
     8  	"math"
     9  	"os"
    10  	"runtime"
    11  	"strconv"
    12  	"time"
    13  
    14  	"github.com/cozy/cozy-stack/model/instance"
    15  	"github.com/cozy/cozy-stack/model/job"
    16  	"github.com/cozy/cozy-stack/pkg/logger"
    17  	"github.com/cozy/cozy-stack/pkg/metrics"
    18  	"github.com/cozy/cozy-stack/pkg/utils"
    19  	"github.com/prometheus/client_golang/prometheus"
    20  	"github.com/spf13/afero"
    21  )
    22  
    23  var defaultTimeout = 300 * time.Second
    24  
    25  func init() {
    26  	job.AddWorker(&job.WorkerConfig{
    27  		WorkerType: "konnector",
    28  		WorkerStart: func(ctx *job.TaskContext) (*job.TaskContext, error) {
    29  			return ctx.WithCookie(&konnectorWorker{}), nil
    30  		},
    31  		BeforeHook:   beforeHookKonnector,
    32  		ErrorHook:    jobHookErrorCheckerKonnector,
    33  		WorkerFunc:   worker,
    34  		WorkerCommit: commit,
    35  		Concurrency:  runtime.NumCPU() * 2,
    36  		MaxExecCount: 2,
    37  		Timeout:      defaultTimeout,
    38  	})
    39  
    40  	job.AddWorker(&job.WorkerConfig{
    41  		WorkerType: "service",
    42  		WorkerStart: func(ctx *job.TaskContext) (*job.TaskContext, error) {
    43  			return ctx.WithCookie(&serviceWorker{}), nil
    44  		},
    45  		WorkerFunc:   worker,
    46  		WorkerCommit: commit,
    47  		Concurrency:  runtime.NumCPU() * 2,
    48  		MaxExecCount: 2,
    49  		Timeout:      defaultTimeout,
    50  	})
    51  }
    52  
    53  type execWorker interface {
    54  	Slug() string
    55  	PrepareWorkDir(ctx *job.TaskContext, i *instance.Instance) (workDir string, cleanDir func(), err error)
    56  	PrepareCmdEnv(ctx *job.TaskContext, i *instance.Instance) (cmd string, env []string, err error)
    57  	ScanOutput(ctx *job.TaskContext, i *instance.Instance, line []byte) error
    58  	Error(i *instance.Instance, err error) error
    59  	Logger(ctx *job.TaskContext) logger.Logger
    60  	Commit(ctx *job.TaskContext, errjob error) error
    61  }
    62  
    63  func worker(ctx *job.TaskContext) (err error) {
    64  	worker := ctx.Cookie().(execWorker)
    65  
    66  	if ctx.Instance == nil {
    67  		return instance.ErrNotFound
    68  	}
    69  
    70  	workDir, cleanDir, err := worker.PrepareWorkDir(ctx, ctx.Instance)
    71  	defer cleanDir()
    72  	if err != nil {
    73  		worker.Logger(ctx).Errorf("PrepareWorkDir: %s", err)
    74  		return err
    75  	}
    76  
    77  	cmdStr, env, err := worker.PrepareCmdEnv(ctx, ctx.Instance)
    78  	if err != nil {
    79  		worker.Logger(ctx).Errorf("PrepareCmdEnv: %s", err)
    80  		return err
    81  	}
    82  
    83  	var stderrBuf bytes.Buffer
    84  	cmd := CreateCmd(cmdStr, workDir)
    85  	cmd.Env = env
    86  
    87  	// set stderr writable with a bytes.Buffer limited total size of 256Ko
    88  	cmd.Stderr = utils.LimitWriterDiscard(&stderrBuf, 256*1024)
    89  
    90  	// Log out all things printed in stderr, whatever the result of the
    91  	// konnector is.
    92  	log := worker.Logger(ctx)
    93  	defer func() {
    94  		if stderrBuf.Len() > 0 {
    95  			log.Errorf("Stderr: %s", stderrBuf.String())
    96  		}
    97  	}()
    98  
    99  	cmdOut, err := cmd.StdoutPipe()
   100  	if err != nil {
   101  		return err
   102  	}
   103  	scanBuf := make([]byte, 16*1024)
   104  	scanOut := bufio.NewScanner(cmdOut)
   105  	scanOut.Buffer(scanBuf, 64*1024)
   106  
   107  	timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
   108  		var result string
   109  		if err != nil {
   110  			result = metrics.WorkerExecResultErrored
   111  		} else {
   112  			result = metrics.WorkerExecResultSuccess
   113  		}
   114  		metrics.WorkersKonnectorsExecDurations.
   115  			WithLabelValues(worker.Slug(), result).
   116  			Observe(v)
   117  	}))
   118  	defer timer.ObserveDuration()
   119  
   120  	if err = cmd.Start(); err != nil {
   121  		return wrapErr(ctx, err)
   122  	}
   123  
   124  	waitDone := make(chan error)
   125  	go func() {
   126  		for scanOut.Scan() {
   127  			if errOut := worker.ScanOutput(ctx, ctx.Instance, scanOut.Bytes()); errOut != nil {
   128  				log.Debug(errOut.Error())
   129  			}
   130  		}
   131  		if errs := scanOut.Err(); errs != nil {
   132  			log.Errorf("could not scan stdout: %s", errs)
   133  		}
   134  		waitDone <- cmd.Wait()
   135  		close(waitDone)
   136  	}()
   137  
   138  	select {
   139  	case err = <-waitDone:
   140  	case <-ctx.Done():
   141  		err = ctx.Err()
   142  		_ = KillCmd(cmd)
   143  		<-waitDone
   144  	}
   145  
   146  	return worker.Error(ctx.Instance, err)
   147  }
   148  
   149  func commit(ctx *job.TaskContext, errjob error) error {
   150  	return ctx.Cookie().(execWorker).Commit(ctx, errjob)
   151  }
   152  
   153  func ctxToTimeLimit(ctx *job.TaskContext) string {
   154  	var limit float64
   155  	if deadline, ok := ctx.Deadline(); ok {
   156  		limit = time.Until(deadline).Seconds()
   157  	}
   158  	if limit <= 0 {
   159  		limit = defaultTimeout.Seconds()
   160  	}
   161  	// add a little gap of 5 seconds to prevent racing the two deadlines
   162  	return strconv.Itoa(int(math.Ceil(limit)) + 5)
   163  }
   164  
   165  func wrapErr(ctx context.Context, err error) error {
   166  	if ctx.Err() == context.DeadlineExceeded {
   167  		return context.DeadlineExceeded
   168  	}
   169  	return err
   170  }
   171  
   172  // MaxPayloadSizeInEnvVar is the maximal size that the COZY_PAYLOAD env
   173  // variable can be. If the payload is larger, we can't put it in the env
   174  // variable as the kernel as a limit for it. Instead, we put the payload in a
   175  // temporary file and only gives the filename in the COZY_PAYLOAD variable.
   176  const MaxPayloadSizeInEnvVar = 100000
   177  
   178  const payloadFilename = "cozy_payload.json"
   179  
   180  func preparePayload(ctx *job.TaskContext, workDir string) (string, error) {
   181  	var payload string
   182  	if p, err := ctx.UnmarshalPayload(); err == nil {
   183  		marshaled, err := json.Marshal(p)
   184  		if err != nil {
   185  			return "", err
   186  		}
   187  		payload = string(marshaled)
   188  	}
   189  
   190  	if len(payload) > MaxPayloadSizeInEnvVar {
   191  		workFS := afero.NewBasePathFs(afero.NewOsFs(), workDir)
   192  		f, err := workFS.OpenFile(payloadFilename, os.O_CREATE|os.O_WRONLY, 0640)
   193  		if err != nil {
   194  			return "", err
   195  		}
   196  		_, err = f.WriteString(payload)
   197  		errc := f.Close()
   198  		if err != nil {
   199  			return "", err
   200  		}
   201  		if errc != nil {
   202  			return "", errc
   203  		}
   204  		payload = "@" + payloadFilename
   205  	}
   206  
   207  	return payload, nil
   208  }