@@ -18,6 +18,7 @@ import (
18
18
tfjson "github.com/hashicorp/terraform-json"
19
19
"golang.org/x/xerrors"
20
20
21
+ "cdr.dev/slog"
21
22
"github.com/coder/coder/provisionersdk/proto"
22
23
)
23
24
@@ -171,10 +172,12 @@ func versionFromBinaryPath(ctx context.Context, binaryPath string) (*version.Ver
171
172
return version .NewVersion (vj .Version )
172
173
}
173
174
174
- func (e executor ) init (ctx , killCtx context.Context , logr logger ) error {
175
+ func (e executor ) init (ctx , killCtx context.Context , logr logSink ) error {
175
176
outWriter , doneOut := logWriter (logr , proto .LogLevel_DEBUG )
176
177
errWriter , doneErr := logWriter (logr , proto .LogLevel_ERROR )
177
178
defer func () {
179
+ _ = outWriter .Close ()
180
+ _ = errWriter .Close ()
178
181
<- doneOut
179
182
<- doneErr
180
183
}()
@@ -201,7 +204,7 @@ func (e executor) init(ctx, killCtx context.Context, logr logger) error {
201
204
}
202
205
203
206
// revive:disable-next-line:flag-parameter
204
- func (e executor ) plan (ctx , killCtx context.Context , env , vars []string , logr logger , destroy bool ) (* proto.Provision_Response , error ) {
207
+ func (e executor ) plan (ctx , killCtx context.Context , env , vars []string , logr logSink , destroy bool ) (* proto.Provision_Response , error ) {
205
208
planfilePath := filepath .Join (e .workdir , "terraform.tfplan" )
206
209
args := []string {
207
210
"plan" ,
@@ -221,6 +224,8 @@ func (e executor) plan(ctx, killCtx context.Context, env, vars []string, logr lo
221
224
outWriter , doneOut := provisionLogWriter (logr )
222
225
errWriter , doneErr := logWriter (logr , proto .LogLevel_ERROR )
223
226
defer func () {
227
+ _ = outWriter .Close ()
228
+ _ = errWriter .Close ()
224
229
<- doneOut
225
230
<- doneErr
226
231
}()
@@ -287,7 +292,7 @@ func (e executor) graph(ctx, killCtx context.Context) (string, error) {
287
292
}
288
293
289
294
// revive:disable-next-line:flag-parameter
290
- func (e executor ) apply (ctx , killCtx context.Context , env , vars []string , logr logger , destroy bool ,
295
+ func (e executor ) apply (ctx , killCtx context.Context , env , vars []string , logr logSink , destroy bool ,
291
296
) (* proto.Provision_Response , error ) {
292
297
args := []string {
293
298
"apply" ,
@@ -307,6 +312,8 @@ func (e executor) apply(ctx, killCtx context.Context, env, vars []string, logr l
307
312
outWriter , doneOut := provisionLogWriter (logr )
308
313
errWriter , doneErr := logWriter (logr , proto .LogLevel_ERROR )
309
314
defer func () {
315
+ _ = outWriter .Close ()
316
+ _ = errWriter .Close ()
310
317
<- doneOut
311
318
<- doneErr
312
319
}()
@@ -380,86 +387,104 @@ func interruptCommandOnCancel(ctx, killCtx context.Context, cmd *exec.Cmd) {
380
387
}()
381
388
}
382
389
383
- type logger interface {
384
- Log (* proto.Log ) error
390
+ type logSink interface {
391
+ Log (* proto.Log )
385
392
}
386
393
387
- type streamLogger struct {
394
+ type streamLogSink struct {
395
+ // Any errors writing to the stream will be logged to logger.
396
+ logger slog.Logger
388
397
stream proto.DRPCProvisioner_ProvisionStream
389
398
}
390
399
391
- func (s streamLogger ) Log (l * proto.Log ) error {
392
- return s .stream .Send (& proto.Provision_Response {
400
+ var _ logSink = streamLogSink {}
401
+
402
+ func (s streamLogSink ) Log (l * proto.Log ) {
403
+ err := s .stream .Send (& proto.Provision_Response {
393
404
Type : & proto.Provision_Response_Log {
394
405
Log : l ,
395
406
},
396
407
})
408
+ if err != nil {
409
+ s .logger .Warn (context .Background (), "write log to stream" ,
410
+ slog .F ("level" , l .Level .String ()),
411
+ slog .F ("message" , l .Output ),
412
+ slog .Error (err ),
413
+ )
414
+ }
397
415
}
398
416
399
417
// logWriter creates a WriteCloser that will log each line of text at the given level. The WriteCloser must be closed
400
418
// by the caller to end logging, after which the returned channel will be closed to indicate that logging of the written
401
419
// data has finished. Failure to close the WriteCloser will leak a goroutine.
402
- func logWriter (logr logger , level proto.LogLevel ) (io.WriteCloser , <- chan any ) {
420
+ func logWriter (sink logSink , level proto.LogLevel ) (io.WriteCloser , <- chan any ) {
403
421
r , w := io .Pipe ()
404
422
done := make (chan any )
405
- go readAndLog (logr , r , done , level )
423
+ go readAndLog (sink , r , done , level )
406
424
return w , done
407
425
}
408
426
409
- func readAndLog (logr logger , r io.Reader , done chan <- any , level proto.LogLevel ) {
427
+ func readAndLog (sink logSink , r io.Reader , done chan <- any , level proto.LogLevel ) {
410
428
defer close (done )
411
429
scanner := bufio .NewScanner (r )
412
430
for scanner .Scan () {
413
- err := logr .Log (& proto.Log {Level : level , Output : scanner .Text ()})
414
- if err != nil {
415
- // Not much we can do. We can't log because logging is itself breaking!
416
- return
417
- }
431
+ sink .Log (& proto.Log {Level : level , Output : scanner .Text ()})
418
432
}
419
433
}
420
434
421
435
// provisionLogWriter creates a WriteCloser that will log each JSON formatted terraform log. The WriteCloser must be
422
436
// closed by the caller to end logging, after which the returned channel will be closed to indicate that logging of the
423
437
// written data has finished. Failure to close the WriteCloser will leak a goroutine.
424
- func provisionLogWriter (logr logger ) (io.WriteCloser , <- chan any ) {
438
+ func provisionLogWriter (sink logSink ) (io.WriteCloser , <- chan any ) {
425
439
r , w := io .Pipe ()
426
440
done := make (chan any )
427
- go provisionReadAndLog (logr , r , done )
441
+ go provisionReadAndLog (sink , r , done )
428
442
return w , done
429
443
}
430
444
431
- func provisionReadAndLog (logr logger , reader io.Reader , done chan <- any ) {
445
+ func provisionReadAndLog (sink logSink , r io.Reader , done chan <- any ) {
432
446
defer close (done )
433
- decoder := json . NewDecoder ( reader )
434
- for {
447
+ scanner := bufio . NewScanner ( r )
448
+ for scanner . Scan () {
435
449
var log terraformProvisionLog
436
- err := decoder . Decode ( & log )
450
+ err := json . Unmarshal ( scanner . Bytes (), & log )
437
451
if err != nil {
438
- return
452
+ // Sometimes terraform doesn't log JSON, even though we asked it to.
453
+ // The terraform maintainers have said on the issue tracker that
454
+ // they don't guarantee that non-JSON lines won't get printed.
455
+ // https://github.com/hashicorp/terraform/issues/29252#issuecomment-887710001
456
+ //
457
+ // > I think as a practical matter it isn't possible for us to
458
+ // > promise that the output will always be entirely JSON, because
459
+ // > there's plenty of code that runs before command line arguments
460
+ // > are parsed and thus before we even know we're in JSON mode.
461
+ // > Given that, I'd suggest writing code that consumes streaming
462
+ // > JSON output from Terraform in such a way that it can tolerate
463
+ // > the output not having JSON in it at all.
464
+ //
465
+ // Log lines such as:
466
+ // - Acquiring state lock. This may take a few moments...
467
+ // - Releasing state lock. This may take a few moments...
468
+ if strings .TrimSpace (scanner .Text ()) == "" {
469
+ continue
470
+ }
471
+ log .Level = "info"
472
+ log .Message = scanner .Text ()
439
473
}
440
- logLevel := convertTerraformLogLevel (log .Level , logr )
441
474
442
- err = logr .Log (& proto.Log {Level : logLevel , Output : log .Message })
443
- if err != nil {
444
- // Not much we can do. We can't log because logging is itself breaking!
445
- return
446
- }
475
+ logLevel := convertTerraformLogLevel (log .Level , sink )
476
+ sink .Log (& proto.Log {Level : logLevel , Output : log .Message })
447
477
478
+ // If the diagnostic is provided, let's provide a bit more info!
448
479
if log .Diagnostic == nil {
449
480
continue
450
481
}
451
-
452
- // If the diagnostic is provided, let's provide a bit more info!
453
- logLevel = convertTerraformLogLevel (log .Diagnostic .Severity , logr )
454
- err = logr .Log (& proto.Log {Level : logLevel , Output : log .Diagnostic .Detail })
455
- if err != nil {
456
- // Not much we can do. We can't log because logging is itself breaking!
457
- return
458
- }
482
+ logLevel = convertTerraformLogLevel (log .Diagnostic .Severity , sink )
483
+ sink .Log (& proto.Log {Level : logLevel , Output : log .Diagnostic .Detail })
459
484
}
460
485
}
461
486
462
- func convertTerraformLogLevel (logLevel string , logr logger ) proto.LogLevel {
487
+ func convertTerraformLogLevel (logLevel string , sink logSink ) proto.LogLevel {
463
488
switch strings .ToLower (logLevel ) {
464
489
case "trace" :
465
490
return proto .LogLevel_TRACE
@@ -472,7 +497,7 @@ func convertTerraformLogLevel(logLevel string, logr logger) proto.LogLevel {
472
497
case "error" :
473
498
return proto .LogLevel_ERROR
474
499
default :
475
- _ = logr .Log (& proto.Log {
500
+ sink .Log (& proto.Log {
476
501
Level : proto .LogLevel_WARN ,
477
502
Output : fmt .Sprintf ("unable to convert log level %s" , logLevel ),
478
503
})
0 commit comments