Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8c8344c

Browse files
fix: tolerate non-json lines in provisionerd logs (#5006)
Co-authored-by: Dean Sheather <[email protected]>
1 parent a25deb9 commit 8c8344c

File tree

3 files changed

+80
-77
lines changed

3 files changed

+80
-77
lines changed

provisioner/terraform/executor.go

Lines changed: 64 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
tfjson "github.com/hashicorp/terraform-json"
1919
"golang.org/x/xerrors"
2020

21+
"cdr.dev/slog"
2122
"github.com/coder/coder/provisionersdk/proto"
2223
)
2324

@@ -171,10 +172,12 @@ func versionFromBinaryPath(ctx context.Context, binaryPath string) (*version.Ver
171172
return version.NewVersion(vj.Version)
172173
}
173174

174-
func (e executor) init(ctx, killCtx context.Context, logr logger) error {
175+
func (e executor) init(ctx, killCtx context.Context, logr logSink) error {
175176
outWriter, doneOut := logWriter(logr, proto.LogLevel_DEBUG)
176177
errWriter, doneErr := logWriter(logr, proto.LogLevel_ERROR)
177178
defer func() {
179+
_ = outWriter.Close()
180+
_ = errWriter.Close()
178181
<-doneOut
179182
<-doneErr
180183
}()
@@ -201,7 +204,7 @@ func (e executor) init(ctx, killCtx context.Context, logr logger) error {
201204
}
202205

203206
// 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) {
205208
planfilePath := filepath.Join(e.workdir, "terraform.tfplan")
206209
args := []string{
207210
"plan",
@@ -221,6 +224,8 @@ func (e executor) plan(ctx, killCtx context.Context, env, vars []string, logr lo
221224
outWriter, doneOut := provisionLogWriter(logr)
222225
errWriter, doneErr := logWriter(logr, proto.LogLevel_ERROR)
223226
defer func() {
227+
_ = outWriter.Close()
228+
_ = errWriter.Close()
224229
<-doneOut
225230
<-doneErr
226231
}()
@@ -287,7 +292,7 @@ func (e executor) graph(ctx, killCtx context.Context) (string, error) {
287292
}
288293

289294
// 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,
291296
) (*proto.Provision_Response, error) {
292297
args := []string{
293298
"apply",
@@ -307,6 +312,8 @@ func (e executor) apply(ctx, killCtx context.Context, env, vars []string, logr l
307312
outWriter, doneOut := provisionLogWriter(logr)
308313
errWriter, doneErr := logWriter(logr, proto.LogLevel_ERROR)
309314
defer func() {
315+
_ = outWriter.Close()
316+
_ = errWriter.Close()
310317
<-doneOut
311318
<-doneErr
312319
}()
@@ -380,86 +387,104 @@ func interruptCommandOnCancel(ctx, killCtx context.Context, cmd *exec.Cmd) {
380387
}()
381388
}
382389

383-
type logger interface {
384-
Log(*proto.Log) error
390+
type logSink interface {
391+
Log(*proto.Log)
385392
}
386393

387-
type streamLogger struct {
394+
type streamLogSink struct {
395+
// Any errors writing to the stream will be logged to logger.
396+
logger slog.Logger
388397
stream proto.DRPCProvisioner_ProvisionStream
389398
}
390399

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{
393404
Type: &proto.Provision_Response_Log{
394405
Log: l,
395406
},
396407
})
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+
}
397415
}
398416

399417
// logWriter creates a WriteCloser that will log each line of text at the given level. The WriteCloser must be closed
400418
// by the caller to end logging, after which the returned channel will be closed to indicate that logging of the written
401419
// 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) {
403421
r, w := io.Pipe()
404422
done := make(chan any)
405-
go readAndLog(logr, r, done, level)
423+
go readAndLog(sink, r, done, level)
406424
return w, done
407425
}
408426

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) {
410428
defer close(done)
411429
scanner := bufio.NewScanner(r)
412430
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()})
418432
}
419433
}
420434

421435
// provisionLogWriter creates a WriteCloser that will log each JSON formatted terraform log. The WriteCloser must be
422436
// closed by the caller to end logging, after which the returned channel will be closed to indicate that logging of the
423437
// 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) {
425439
r, w := io.Pipe()
426440
done := make(chan any)
427-
go provisionReadAndLog(logr, r, done)
441+
go provisionReadAndLog(sink, r, done)
428442
return w, done
429443
}
430444

431-
func provisionReadAndLog(logr logger, reader io.Reader, done chan<- any) {
445+
func provisionReadAndLog(sink logSink, r io.Reader, done chan<- any) {
432446
defer close(done)
433-
decoder := json.NewDecoder(reader)
434-
for {
447+
scanner := bufio.NewScanner(r)
448+
for scanner.Scan() {
435449
var log terraformProvisionLog
436-
err := decoder.Decode(&log)
450+
err := json.Unmarshal(scanner.Bytes(), &log)
437451
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()
439473
}
440-
logLevel := convertTerraformLogLevel(log.Level, logr)
441474

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})
447477

478+
// If the diagnostic is provided, let's provide a bit more info!
448479
if log.Diagnostic == nil {
449480
continue
450481
}
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})
459484
}
460485
}
461486

462-
func convertTerraformLogLevel(logLevel string, logr logger) proto.LogLevel {
487+
func convertTerraformLogLevel(logLevel string, sink logSink) proto.LogLevel {
463488
switch strings.ToLower(logLevel) {
464489
case "trace":
465490
return proto.LogLevel_TRACE
@@ -472,7 +497,7 @@ func convertTerraformLogLevel(logLevel string, logr logger) proto.LogLevel {
472497
case "error":
473498
return proto.LogLevel_ERROR
474499
default:
475-
_ = logr.Log(&proto.Log{
500+
sink.Log(&proto.Log{
476501
Level: proto.LogLevel_WARN,
477502
Output: fmt.Sprintf("unable to convert log level %s", logLevel),
478503
})

provisioner/terraform/executor_internal_test.go

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,24 @@ import (
44
"testing"
55

66
"github.com/stretchr/testify/require"
7-
"golang.org/x/xerrors"
87

98
"github.com/coder/coder/provisionersdk/proto"
109
)
1110

1211
type mockLogger struct {
13-
logs []*proto.Log
14-
retVal error
12+
logs []*proto.Log
1513
}
1614

17-
func (m *mockLogger) Log(l *proto.Log) error {
15+
var _ logSink = &mockLogger{}
16+
17+
func (m *mockLogger) Log(l *proto.Log) {
1818
m.logs = append(m.logs, l)
19-
return m.retVal
2019
}
2120

2221
func TestLogWriter_Mainline(t *testing.T) {
2322
t.Parallel()
2423

25-
logr := &mockLogger{retVal: nil}
24+
logr := &mockLogger{}
2625
writer, doneLogging := logWriter(logr, proto.LogLevel_INFO)
2726

2827
_, err := writer.Write([]byte(`Sitting in an English garden
@@ -40,23 +39,5 @@ From standing in the English rain`))
4039
{Level: proto.LogLevel_INFO, Output: "If the sun don't come you get a tan"},
4140
{Level: proto.LogLevel_INFO, Output: "From standing in the English rain"},
4241
}
43-
require.Equal(t, logr.logs, expected)
44-
}
45-
46-
func TestLogWriter_SendError(t *testing.T) {
47-
t.Parallel()
48-
49-
logr := &mockLogger{retVal: xerrors.New("Goo goo g'joob")}
50-
writer, doneLogging := logWriter(logr, proto.LogLevel_INFO)
51-
52-
_, err := writer.Write([]byte(`Sitting in an English garden
53-
Waiting for the sun
54-
If the sun don't come you get a tan
55-
From standing in the English rain`))
56-
require.NoError(t, err)
57-
err = writer.Close()
58-
require.NoError(t, err)
59-
<-doneLogging
60-
expected := []*proto.Log{{Level: proto.LogLevel_INFO, Output: "Sitting in an English garden"}}
61-
require.Equal(t, logr.logs, expected)
42+
require.Equal(t, expected, logr.logs)
6243
}

provisioner/terraform/provision.go

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,17 @@ func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
6969
}
7070
}()
7171

72-
logr := streamLogger{stream: stream}
72+
sink := streamLogSink{
73+
logger: s.logger.Named("execution_logs"),
74+
stream: stream,
75+
}
7376
start := request.GetStart()
7477

7578
e := s.executor(start.Directory)
7679
if err = e.checkMinVersion(ctx); err != nil {
7780
return err
7881
}
79-
if err = logTerraformEnvVars(logr); err != nil {
80-
return err
81-
}
82+
logTerraformEnvVars(sink)
8283

8384
statefilePath := filepath.Join(start.Directory, "terraform.tfstate")
8485
if len(start.State) > 0 {
@@ -111,7 +112,7 @@ func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
111112
}
112113

113114
s.logger.Debug(ctx, "running initialization")
114-
err = e.init(ctx, killCtx, logr)
115+
err = e.init(ctx, killCtx, sink)
115116
if err != nil {
116117
if ctx.Err() != nil {
117118
return stream.Send(&proto.Provision_Response{
@@ -136,10 +137,10 @@ func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
136137
}
137138
var resp *proto.Provision_Response
138139
if start.DryRun {
139-
resp, err = e.plan(ctx, killCtx, env, vars, logr,
140+
resp, err = e.plan(ctx, killCtx, env, vars, sink,
140141
start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY)
141142
} else {
142-
resp, err = e.apply(ctx, killCtx, env, vars, logr,
143+
resp, err = e.apply(ctx, killCtx, env, vars, sink,
143144
start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY)
144145
}
145146
if err != nil {
@@ -231,7 +232,7 @@ var (
231232
}
232233
)
233234

234-
func logTerraformEnvVars(logr logger) error {
235+
func logTerraformEnvVars(sink logSink) {
235236
env := safeEnviron()
236237
for _, e := range env {
237238
if strings.HasPrefix(e, "TF_") {
@@ -242,14 +243,10 @@ func logTerraformEnvVars(logr logger) error {
242243
if !tfEnvSafeToPrint[parts[0]] {
243244
parts[1] = "<value redacted>"
244245
}
245-
err := logr.Log(&proto.Log{
246+
sink.Log(&proto.Log{
246247
Level: proto.LogLevel_WARN,
247248
Output: fmt.Sprintf("terraform environment variable: %s=%s", parts[0], parts[1]),
248249
})
249-
if err != nil {
250-
return err
251-
}
252250
}
253251
}
254-
return nil
255252
}

0 commit comments

Comments
 (0)