get.porter.sh/porter@v1.3.0/pkg/portercontext/context.go (about)

     1  package portercontext
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  
    14  	"get.porter.sh/porter/pkg"
    15  	"get.porter.sh/porter/pkg/tracing"
    16  	"github.com/carolynvs/aferox"
    17  	cnabclaims "github.com/cnabio/cnab-go/claim"
    18  	"github.com/hashicorp/go-multierror"
    19  	"github.com/mattn/go-colorable"
    20  	"github.com/mattn/go-isatty"
    21  	"github.com/spf13/afero"
    22  	"go.opentelemetry.io/otel/attribute"
    23  	"go.uber.org/zap"
    24  	"go.uber.org/zap/buffer"
    25  	"go.uber.org/zap/zapcore"
    26  )
    27  
    28  const (
    29  	// MixinOutputsDir represents the directory where mixin output files are written/read
    30  	MixinOutputsDir = "/cnab/app/porter/outputs"
    31  
    32  	// EnvCorrelationID is the name of the environment variable containing the
    33  	// id to correlate logs with a workflow.
    34  	EnvCorrelationID = "PORTER_CORRELATION_ID"
    35  )
    36  
    37  type CommandBuilder func(ctx context.Context, name string, arg ...string) *exec.Cmd
    38  
    39  // Context should not be used in concurrent code, it is single threaded.
    40  type Context struct {
    41  	environ            map[string]string
    42  	FileSystem         aferox.Aferox
    43  	In                 io.Reader
    44  	Out                io.Writer
    45  	Err                io.Writer
    46  	NewCommand         CommandBuilder
    47  	PlugInDebugContext *PluginDebugContext
    48  
    49  	//
    50  	// Logging and Tracing configuration
    51  	//
    52  
    53  	// a consistent id that is set on the context and emitted in the logs
    54  	// Helps correlate logs with a workflow.
    55  	correlationId string
    56  
    57  	// logCfg is the logger configuration used.
    58  	logCfg LogConfiguration
    59  
    60  	// logFile is the open file where we are sending logs.
    61  	logFile afero.File
    62  
    63  	// traceFile is the open file where we are sending traces.
    64  	traceFile afero.File
    65  
    66  	// indicates if log timestamps should be printed to the console
    67  	timestampLogs bool
    68  
    69  	// handles sending tracing data to an otel collector
    70  	tracer tracing.Tracer
    71  
    72  	// indicates if we have created a real tracer yet (instead of noop)
    73  	tracerInitalized bool
    74  
    75  	// handles send log data to the console/logfile
    76  	logger *zap.Logger
    77  
    78  	// IsInternalPlugin indicates that Porter is running as an internal plugin
    79  	IsInternalPlugin bool
    80  
    81  	// InternalPluginKey is the current plugin that Porter is running as, e.g. storage.porter.mongodb
    82  	InternalPluginKey string
    83  
    84  	censoredWriter *CensoredWriter
    85  }
    86  
    87  // New creates a new context in the specified directory.
    88  func New() *Context {
    89  	// Ignore any error getting the working directory and report errors
    90  	// when we attempt to access files in the current directory. This
    91  	// allows us to use the current directory as a default, and allow
    92  	// tests to override it.
    93  	pwd, _ := os.Getwd()
    94  
    95  	correlationId := os.Getenv(EnvCorrelationID)
    96  	if correlationId == "" {
    97  		correlationId = cnabclaims.MustNewULID() // not using cnab package because that creates a cycle
    98  	}
    99  
   100  	c := &Context{
   101  		environ:        getEnviron(),
   102  		FileSystem:     aferox.NewAferox(pwd, afero.NewOsFs()),
   103  		In:             os.Stdin,
   104  		Out:            NewCensoredWriter(os.Stdout),
   105  		Err:            NewCensoredWriter(os.Stderr),
   106  		correlationId:  correlationId,
   107  		timestampLogs:  true,
   108  		censoredWriter: NewCensoredWriter(os.Stdout),
   109  	}
   110  
   111  	// Make the correlation id available for the plugins to use
   112  	c.Setenv(EnvCorrelationID, correlationId)
   113  
   114  	c.ConfigureLogging(context.Background(), LogConfiguration{})
   115  	c.defaultNewCommand()
   116  	c.PlugInDebugContext = NewPluginDebugContext(c)
   117  
   118  	return c
   119  }
   120  
   121  // StartRootSpan creates the root tracing span for the porter application.
   122  // This should only be done once.
   123  func (c *Context) StartRootSpan(ctx context.Context, op string, attrs ...attribute.KeyValue) (context.Context, tracing.RootTraceLogger) {
   124  	childCtx, span := c.tracer.Start(ctx, op)
   125  	attrs = append(attrs, attribute.String("correlation-id", c.correlationId))
   126  	span.SetAttributes(attrs...)
   127  	return tracing.NewRootLogger(childCtx, span, c.logger, c.tracer)
   128  }
   129  
   130  func (c *Context) makeLogEncoding() zapcore.EncoderConfig {
   131  	enc := zap.NewProductionEncoderConfig()
   132  	if c.timestampLogs {
   133  		enc.EncodeTime = zapcore.ISO8601TimeEncoder
   134  	} else { // used for testing, so we don't have unique timestamps in the logs
   135  		enc.EncodeTime = func(time time.Time, encoder zapcore.PrimitiveArrayEncoder) {
   136  			encoder.AppendString("")
   137  		}
   138  	}
   139  
   140  	return enc
   141  }
   142  
   143  type LogConfiguration struct {
   144  	// Verbosity is the threshold for printing messages to the console.
   145  	Verbosity zapcore.Level
   146  
   147  	LogToFile    bool
   148  	LogDirectory string
   149  
   150  	// LogLevel is the threshold for writing messages to the log file.
   151  	LogLevel                zapcore.Level
   152  	StructuredLogs          bool
   153  	TelemetryEnabled        bool
   154  	TelemetryEndpoint       string
   155  	TelemetryProtocol       string
   156  	TelemetryInsecure       bool
   157  	TelemetryCertificate    string
   158  	TelemetryCompression    string
   159  	TelemetryTimeout        string
   160  	TelemetryHeaders        map[string]string
   161  	TelemetryServiceName    string
   162  	TelemetryDirectory      string
   163  	TelemetryRedirectToFile bool
   164  	TelemetryStartTimeout   time.Duration
   165  }
   166  
   167  // ConfigureLogging applies different configuration to our logging and tracing.
   168  // Returns an updated context with the configured logger applied
   169  func (c *Context) ConfigureLogging(ctx context.Context, cfg LogConfiguration) context.Context {
   170  	c.logCfg = cfg
   171  
   172  	var baseLogger zapcore.Core
   173  	if c.IsInternalPlugin {
   174  		c.logCfg.TelemetryServiceName = c.InternalPluginKey
   175  		baseLogger = c.makePluginLogger(c.InternalPluginKey, cfg)
   176  	} else {
   177  		baseLogger = c.makeConsoleLogger()
   178  	}
   179  
   180  	c.configureLoggingWith(ctx, baseLogger)
   181  
   182  	return tracing.UpdateRootLogger(ctx, c.logger)
   183  }
   184  
   185  // ConfigureLogging applies different configuration to our logging and tracing.
   186  func (c *Context) configureLoggingWith(ctx context.Context, baseLogger zapcore.Core) {
   187  	// make a temporary logger that we can use until we've completely initialized the full logger
   188  	tmpLog := zap.New(baseLogger)
   189  
   190  	var err error
   191  	fileLogger := zapcore.NewNopCore()
   192  	if c.logCfg.LogToFile {
   193  		fileLogger, err = c.configureFileLog(c.logCfg.LogDirectory)
   194  		if err != nil {
   195  			tmpLog.Error(fmt.Errorf("could not configure a file logger: %w", err).Error())
   196  		} else {
   197  			tmpLog.Debug("Writing logs to " + c.logFile.Name())
   198  		}
   199  	}
   200  	tmpLog = zap.New(zapcore.NewTee(baseLogger, fileLogger))
   201  
   202  	if c.logCfg.TelemetryEnabled {
   203  		// Only initialize the tracer once per command
   204  		if !c.tracerInitalized {
   205  			err = c.configureTelemetry(ctx, c.logCfg, tmpLog)
   206  			if err != nil {
   207  				tmpLog.Error(fmt.Errorf("could not configure a tracer: %w", err).Error())
   208  			}
   209  		}
   210  	} else {
   211  		tracer := createNoopTracer()
   212  		c.tracer = tracer
   213  	}
   214  
   215  	c.logger = tmpLog
   216  }
   217  
   218  func (c *Context) makeConsoleLogger() zapcore.Core {
   219  	encoding := c.makeLogEncoding()
   220  
   221  	stdout := c.Out
   222  	stderr := c.Err
   223  	if f, ok := stdout.(*os.File); ok {
   224  		if isatty.IsTerminal(f.Fd()) {
   225  			stdout = colorable.NewColorable(f)
   226  			encoding.EncodeLevel = zapcore.LowercaseColorLevelEncoder
   227  		}
   228  	}
   229  
   230  	if f, ok := stderr.(*os.File); ok {
   231  		if isatty.IsTerminal(f.Fd()) {
   232  			stdout = colorable.NewColorable(f)
   233  			encoding.EncodeLevel = zapcore.LowercaseColorLevelEncoder
   234  		}
   235  	}
   236  
   237  	// if structured-logs feature isn't enabled, keep the logs looking like they do now, with just the message printed
   238  	if !c.logCfg.StructuredLogs {
   239  		encoding.TimeKey = ""
   240  		encoding.LevelKey = ""
   241  	}
   242  	censoredEncoder := &CensoredEncoder{Encoder: zapcore.NewConsoleEncoder(encoding), censoredWriter: c.censoredWriter}
   243  
   244  	isInformational := func(lvl zapcore.Level) bool {
   245  		return lvl < zapcore.ErrorLevel && lvl >= c.logCfg.Verbosity
   246  	}
   247  	stdoutEnabler := zap.LevelEnablerFunc(isInformational)
   248  	stderrEnabler := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
   249  		return !isInformational(lvl) && lvl >= zapcore.ErrorLevel
   250  	})
   251  
   252  	return zapcore.NewTee(
   253  		zapcore.NewCore(censoredEncoder, zapcore.AddSync(stdout), stdoutEnabler),
   254  		zapcore.NewCore(censoredEncoder, zapcore.AddSync(stderr), stderrEnabler),
   255  	)
   256  }
   257  
   258  func (c *Context) configureFileLog(dir string) (zapcore.Core, error) {
   259  	if err := c.FileSystem.MkdirAll(dir, pkg.FileModeDirectory); err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	// Write the logs to a file
   264  	logfile := filepath.Join(dir, c.buildLogFileName())
   265  	if c.logFile == nil { // We may have already opened this logfile, and we are just changing the log level
   266  		f, err := c.FileSystem.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, pkg.FileModeWritable)
   267  		if err != nil {
   268  			return zapcore.NewNopCore(), fmt.Errorf("could not start log file at %s: %w", logfile, err)
   269  		}
   270  		c.logFile = f
   271  	}
   272  
   273  	// Split logs to the console and file
   274  	fileEncoder := zapcore.NewJSONEncoder(c.makeLogEncoding())
   275  	censoredEncoder := &CensoredEncoder{Encoder: fileEncoder, censoredWriter: c.censoredWriter}
   276  	return zapcore.NewCore(censoredEncoder, zapcore.AddSync(c.logFile), c.logCfg.LogLevel), nil
   277  }
   278  
   279  func (c *Context) buildLogFileName() string {
   280  	// Send plugin logs and traces to a separate file so that there aren't conflicts while writing
   281  	if c.IsInternalPlugin {
   282  		return fmt.Sprintf("%s-%s.json", c.correlationId, c.InternalPluginKey)
   283  	}
   284  	return fmt.Sprintf("%s.json", c.correlationId)
   285  }
   286  
   287  func (c *Context) Close() error {
   288  	var bigErr *multierror.Error
   289  
   290  	if err := c.tracer.Close(context.Background()); err != nil {
   291  		err = fmt.Errorf("error closing tracer: %w", err)
   292  		bigErr = multierror.Append(bigErr, err)
   293  	}
   294  
   295  	if c.traceFile != nil {
   296  		if err := c.traceFile.Close(); err != nil {
   297  			err = fmt.Errorf("error closing trace file %s: %w", c.traceFile.Name(), err)
   298  			bigErr = multierror.Append(bigErr, err)
   299  		}
   300  		c.traceFile = nil
   301  	}
   302  
   303  	if c.logFile != nil {
   304  		if err := c.logFile.Close(); err != nil {
   305  			err = fmt.Errorf("error closing log file %s: %w", c.logFile.Name(), err)
   306  			bigErr = multierror.Append(bigErr, err)
   307  		}
   308  		c.logFile = nil
   309  	}
   310  
   311  	return bigErr.ErrorOrNil()
   312  }
   313  
   314  func (c *Context) defaultNewCommand() {
   315  	c.NewCommand = func(ctx context.Context, name string, arg ...string) *exec.Cmd {
   316  		return c.CommandContext(ctx, name, arg...)
   317  	}
   318  }
   319  
   320  // CommandContext creates a new exec.Cmd in the current directory.
   321  // The provided context is used to kill the process (by calling
   322  // os.Process.Kill) if the context becomes done before the command
   323  // completes on its own.
   324  func (c *Context) CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd {
   325  	if filepath.Base(name) == name {
   326  		if lp, ok := c.LookPath(name); ok {
   327  			name = lp
   328  		}
   329  	}
   330  
   331  	cmd := exec.CommandContext(ctx, name, arg...)
   332  	cmd.Dir = c.Getwd()
   333  	cmd.Env = c.Environ()
   334  	return cmd
   335  }
   336  
   337  func getEnviron() map[string]string {
   338  	environ := map[string]string{}
   339  	for _, env := range os.Environ() {
   340  		envParts := strings.SplitN(env, "=", 2)
   341  		key := envParts[0]
   342  		value := ""
   343  		if len(envParts) > 1 {
   344  			value = envParts[1]
   345  		}
   346  		environ[key] = value
   347  	}
   348  	return environ
   349  }
   350  
   351  // Environ returns a copy of strings representing the environment,
   352  // in the form "key=value".
   353  func (c *Context) Environ() []string {
   354  	e := make([]string, 0, len(c.environ))
   355  	for k, v := range c.environ {
   356  		e = append(e, fmt.Sprintf("%s=%s", k, v))
   357  	}
   358  	return e
   359  }
   360  
   361  // EnvironMap returns a map of the current environment variables.
   362  func (c *Context) EnvironMap() map[string]string {
   363  	env := make(map[string]string, len(c.environ))
   364  	for k, v := range c.environ {
   365  		env[k] = v
   366  	}
   367  	return env
   368  }
   369  
   370  // ExpandEnv replaces ${var} or $var in the string according to the values
   371  // of the current environment variables. References to undefined
   372  // variables are replaced by the empty string.
   373  func (c *Context) ExpandEnv(s string) string {
   374  	return os.Expand(s, func(key string) string { return c.Getenv(key) })
   375  }
   376  
   377  // Getenv retrieves the value of the environment variable named by the key.
   378  // It returns the value, which will be empty if the variable is not present.
   379  // To distinguish between an empty value and an unset value, use LookupEnv.
   380  func (c *Context) Getenv(key string) string {
   381  	return c.environ[key]
   382  }
   383  
   384  // This is a simplified exec.LookPath that checks if command is accessible given
   385  // a PATH environment variable.
   386  func (c *Context) LookPath(file string) (string, bool) {
   387  	return c.FileSystem.LookPath(file, c.Getenv("PATH"), c.Getenv("PATHEXT"))
   388  }
   389  
   390  // LookupEnv retrieves the value of the environment variable named
   391  // by the key. If the variable is present in the environment the
   392  // value (which may be empty) is returned and the boolean is true.
   393  // Otherwise the returned value will be empty and the boolean will
   394  // be false.
   395  func (c *Context) LookupEnv(key string) (string, bool) {
   396  	value, ok := c.environ[key]
   397  	return value, ok
   398  }
   399  
   400  // Setenv sets the value of the environment variable named by the key.
   401  // It returns an error, if any.
   402  func (c *Context) Setenv(key string, value string) {
   403  	if c.environ == nil {
   404  		c.environ = make(map[string]string, 1)
   405  	}
   406  
   407  	c.environ[key] = value
   408  }
   409  
   410  // Unsetenv unsets a single environment variable.
   411  func (c *Context) Unsetenv(key string) {
   412  	delete(c.environ, key)
   413  }
   414  
   415  // Clearenv deletes all environment variables.
   416  func (c *Context) Clearenv() {
   417  	c.environ = make(map[string]string, 0)
   418  }
   419  
   420  // Getwd returns a rooted path name corresponding to the current directory.
   421  func (c *Context) Getwd() string {
   422  	return c.FileSystem.Getwd()
   423  }
   424  
   425  // Chdir changes the current working directory to the named directory.
   426  func (c *Context) Chdir(dir string) {
   427  	c.FileSystem.Chdir(dir)
   428  }
   429  
   430  // CensoredWriter is a writer wrapping the provided io.Writer with logic to censor certain values
   431  type CensoredWriter struct {
   432  	writer          io.Writer
   433  	sensitiveValues []string
   434  }
   435  
   436  // NewCensoredWriter returns a new CensoredWriter
   437  func NewCensoredWriter(writer io.Writer) *CensoredWriter {
   438  	return &CensoredWriter{writer: writer, sensitiveValues: []string{}}
   439  }
   440  
   441  // SetSensitiveValues sets values needing masking for an CensoredWriter
   442  func (cw *CensoredWriter) SetSensitiveValues(vals []string) {
   443  	cw.sensitiveValues = vals
   444  }
   445  
   446  // Write implements io.Writer's Write method, performing necessary auditing while doing so
   447  func (cw *CensoredWriter) Write(b []byte) (int, error) {
   448  	auditedBytes := b
   449  	for _, val := range cw.sensitiveValues {
   450  		if strings.TrimSpace(val) != "" {
   451  			auditedBytes = bytes.Replace(auditedBytes, []byte(val), []byte("*******"), -1)
   452  		}
   453  	}
   454  
   455  	_, err := cw.writer.Write(auditedBytes)
   456  	return len(b), err
   457  }
   458  
   459  func (cw *CensoredWriter) Censor(b []byte) []byte {
   460  	for _, val := range cw.sensitiveValues {
   461  		if strings.TrimSpace(val) != "" {
   462  			b = bytes.Replace(b, []byte(val), []byte("*******"), -1)
   463  		}
   464  	}
   465  
   466  	return b
   467  }
   468  
   469  func (c *Context) CopyDirectory(srcDir, destDir string, includeBaseDir bool) error {
   470  	var stripPrefix string
   471  	if includeBaseDir {
   472  		stripPrefix = filepath.Dir(srcDir)
   473  	} else {
   474  		stripPrefix = srcDir
   475  	}
   476  
   477  	return c.FileSystem.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
   478  		if err != nil {
   479  			return err
   480  		}
   481  
   482  		// Translate the path from the src to the final destination
   483  		dest := filepath.Join(destDir, strings.TrimPrefix(path, stripPrefix))
   484  		if dest == "" {
   485  			return nil
   486  		}
   487  
   488  		if info.IsDir() {
   489  			err := c.FileSystem.MkdirAll(dest, info.Mode())
   490  			if err != nil {
   491  				return err
   492  			}
   493  
   494  			return nil
   495  		}
   496  
   497  		return c.CopyFile(path, dest)
   498  	})
   499  }
   500  
   501  func (c *Context) CopyFile(src, dest string) error {
   502  	info, err := c.FileSystem.Stat(src)
   503  	if err != nil {
   504  		return err
   505  	}
   506  
   507  	data, err := c.FileSystem.ReadFile(src)
   508  	if err != nil {
   509  		return err
   510  	}
   511  
   512  	err = c.FileSystem.WriteFile(dest, data, info.Mode())
   513  	if err != nil {
   514  		return err
   515  	}
   516  
   517  	return nil
   518  }
   519  
   520  // WriteMixinOutputToFile writes the provided bytes (representing a mixin output)
   521  // to a file named by the provided filename in Porter's mixin outputs directory
   522  func (c *Context) WriteMixinOutputToFile(filename string, bytes []byte) error {
   523  	exists, err := c.FileSystem.DirExists(MixinOutputsDir)
   524  	if err != nil {
   525  		return err
   526  	}
   527  	if !exists {
   528  		if err := c.FileSystem.MkdirAll(MixinOutputsDir, pkg.FileModeDirectory); err != nil {
   529  			return fmt.Errorf("couldn't make output directory: %w", err)
   530  		}
   531  	}
   532  
   533  	return c.FileSystem.WriteFile(filepath.Join(MixinOutputsDir, filename), bytes, pkg.FileModeWritable)
   534  }
   535  
   536  // SetSensitiveValues sets the sensitive values needing masking on output/err streams
   537  // WARNING: This does not work if you are writing to the TraceLogger.
   538  // See https://github.com/getporter/porter/issues/2256
   539  // Only use this when you are calling fmt.Fprintln, not log.Debug, etc.
   540  func (c *Context) SetSensitiveValues(vals []string) {
   541  	if len(vals) > 0 {
   542  		out := NewCensoredWriter(c.Out)
   543  		out.SetSensitiveValues(vals)
   544  		c.Out = out
   545  		c.censoredWriter.SetSensitiveValues(vals)
   546  
   547  		err := NewCensoredWriter(c.Err)
   548  		err.SetSensitiveValues(vals)
   549  		c.Err = err
   550  	}
   551  }
   552  
   553  type CensoredEncoder struct {
   554  	zapcore.Encoder
   555  	censoredWriter *CensoredWriter
   556  }
   557  
   558  func (ce CensoredEncoder) Clone() zapcore.Encoder {
   559  	return &CensoredEncoder{Encoder: ce.Encoder.Clone(), censoredWriter: ce.censoredWriter}
   560  }
   561  
   562  func (ce CensoredEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
   563  	entry.Message = string(ce.censoredWriter.Censor([]byte(entry.Message)))
   564  	for _, field := range fields {
   565  		if field.Type == zapcore.StringType {
   566  			field.String = string(ce.censoredWriter.Censor([]byte(field.String)))
   567  		}
   568  	}
   569  
   570  	return ce.Encoder.EncodeEntry(entry, fields)
   571  }