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 }