Skip to content

Commit ce4a9fb

Browse files
committed
Add logging to provision jobs
1 parent 5d16f2a commit ce4a9fb

File tree

11 files changed

+1042
-458
lines changed

11 files changed

+1042
-458
lines changed

provisioner/terraform/parse.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package terraform
22

33
import (
4-
"context"
54
"encoding/json"
65
"os"
76

@@ -12,24 +11,30 @@ import (
1211
)
1312

1413
// Parse extracts Terraform variables from source-code.
15-
func (*terraform) Parse(_ context.Context, request *proto.Parse_Request) (*proto.Parse_Response, error) {
14+
func (*terraform) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_ParseStream) error {
15+
defer stream.CloseSend()
16+
1617
module, diags := tfconfig.LoadModule(request.Directory)
1718
if diags.HasErrors() {
18-
return nil, xerrors.Errorf("load module: %w", diags.Err())
19+
return xerrors.Errorf("load module: %w", diags.Err())
1920
}
2021
parameters := make([]*proto.ParameterSchema, 0, len(module.Variables))
2122
for _, v := range module.Variables {
2223
schema, err := convertVariableToParameter(v)
2324
if err != nil {
24-
return nil, xerrors.Errorf("convert variable %q: %w", v.Name, err)
25+
return xerrors.Errorf("convert variable %q: %w", v.Name, err)
2526
}
2627

2728
parameters = append(parameters, schema)
2829
}
2930

30-
return &proto.Parse_Response{
31-
ParameterSchemas: parameters,
32-
}, nil
31+
return stream.Send(&proto.Parse_Response{
32+
Type: &proto.Parse_Response_Complete{
33+
Complete: &proto.Parse_Complete{
34+
ParameterSchemas: parameters,
35+
},
36+
},
37+
})
3338
}
3439

3540
// Converts a Terraform variable to a provisioner parameter.

provisioner/terraform/parse_test.go

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,14 @@ func TestParse(t *testing.T) {
4949
}`,
5050
},
5151
Response: &proto.Parse_Response{
52-
ParameterSchemas: []*proto.ParameterSchema{{
53-
Name: "A",
54-
Description: "Testing!",
55-
}},
52+
Type: &proto.Parse_Response_Complete{
53+
Complete: &proto.Parse_Complete{
54+
ParameterSchemas: []*proto.ParameterSchema{{
55+
Name: "A",
56+
Description: "Testing!",
57+
}},
58+
},
59+
},
5660
},
5761
}, {
5862
Name: "default-variable-value",
@@ -62,17 +66,21 @@ func TestParse(t *testing.T) {
6266
}`,
6367
},
6468
Response: &proto.Parse_Response{
65-
ParameterSchemas: []*proto.ParameterSchema{{
66-
Name: "A",
67-
DefaultSource: &proto.ParameterSource{
68-
Scheme: proto.ParameterSource_DATA,
69-
Value: "\"wow\"",
69+
Type: &proto.Parse_Response_Complete{
70+
Complete: &proto.Parse_Complete{
71+
ParameterSchemas: []*proto.ParameterSchema{{
72+
Name: "A",
73+
DefaultSource: &proto.ParameterSource{
74+
Scheme: proto.ParameterSource_DATA,
75+
Value: "\"wow\"",
76+
},
77+
DefaultDestination: &proto.ParameterDestination{
78+
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
79+
Value: "A",
80+
},
81+
}},
7082
},
71-
DefaultDestination: &proto.ParameterDestination{
72-
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
73-
Value: "A",
74-
},
75-
}},
83+
},
7684
},
7785
}, {
7886
Name: "variable-validation",
@@ -84,10 +92,15 @@ func TestParse(t *testing.T) {
8492
}`,
8593
},
8694
Response: &proto.Parse_Response{
87-
ParameterSchemas: []*proto.ParameterSchema{{
88-
Name: "A",
89-
ValidationCondition: `var.A == "value"`,
90-
}},
95+
Type: &proto.Parse_Response_Complete{
96+
Complete: &proto.Parse_Complete{
97+
ParameterSchemas: []*proto.ParameterSchema{{
98+
Name: "A",
99+
ValidationCondition: `var.A == "value"`,
100+
},
101+
},
102+
},
103+
},
91104
},
92105
}} {
93106
testCase := testCase
@@ -106,13 +119,23 @@ func TestParse(t *testing.T) {
106119
})
107120
require.NoError(t, err)
108121

109-
// Ensure the want and got are equivalent!
110-
want, err := json.Marshal(testCase.Response)
111-
require.NoError(t, err)
112-
got, err := json.Marshal(response)
113-
require.NoError(t, err)
122+
for {
123+
msg, err := response.Recv()
124+
require.NoError(t, err)
125+
126+
if msg.GetComplete() == nil {
127+
continue
128+
}
114129

115-
require.Equal(t, string(want), string(got))
130+
// Ensure the want and got are equivalent!
131+
want, err := json.Marshal(testCase.Response)
132+
require.NoError(t, err)
133+
got, err := json.Marshal(msg)
134+
require.NoError(t, err)
135+
136+
require.Equal(t, string(want), string(got))
137+
break
138+
}
116139
})
117140
}
118141
}

provisioner/terraform/provision.go

Lines changed: 123 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package terraform
22

33
import (
4-
"context"
4+
"bufio"
5+
"encoding/json"
56
"fmt"
7+
"io"
68
"os"
79
"path/filepath"
10+
"strings"
811

912
"github.com/hashicorp/terraform-exec/tfexec"
1013
"golang.org/x/xerrors"
@@ -13,28 +16,49 @@ import (
1316
)
1417

1518
// Provision executes `terraform apply`.
16-
func (t *terraform) Provision(ctx context.Context, request *proto.Provision_Request) (*proto.Provision_Response, error) {
19+
func (t *terraform) Provision(request *proto.Provision_Request, stream proto.DRPCProvisioner_ProvisionStream) error {
20+
// defer stream.CloseSend()
21+
ctx := stream.Context()
1722
statefilePath := filepath.Join(request.Directory, "terraform.tfstate")
18-
err := os.WriteFile(statefilePath, request.State, 0600)
19-
if err != nil {
20-
return nil, xerrors.Errorf("write statefile %q: %w", statefilePath, err)
23+
if len(request.State) > 0 {
24+
err := os.WriteFile(statefilePath, request.State, 0600)
25+
if err != nil {
26+
return xerrors.Errorf("write statefile %q: %w", statefilePath, err)
27+
}
2128
}
2229

2330
terraform, err := tfexec.NewTerraform(request.Directory, t.binaryPath)
2431
if err != nil {
25-
return nil, xerrors.Errorf("create new terraform executor: %w", err)
32+
return xerrors.Errorf("create new terraform executor: %w", err)
2633
}
2734
version, _, err := terraform.Version(ctx, false)
2835
if err != nil {
29-
return nil, xerrors.Errorf("get terraform version: %w", err)
36+
return xerrors.Errorf("get terraform version: %w", err)
3037
}
3138
if !version.GreaterThanOrEqual(minimumTerraformVersion) {
32-
return nil, xerrors.Errorf("terraform version %q is too old. required >= %q", version.String(), minimumTerraformVersion.String())
39+
return xerrors.Errorf("terraform version %q is too old. required >= %q", version.String(), minimumTerraformVersion.String())
3340
}
3441

42+
reader, writer := io.Pipe()
43+
defer reader.Close()
44+
defer writer.Close()
45+
go func() {
46+
scanner := bufio.NewScanner(reader)
47+
for scanner.Scan() {
48+
_ = stream.Send(&proto.Provision_Response{
49+
Type: &proto.Provision_Response_Log{
50+
Log: &proto.Log{
51+
Level: proto.LogLevel_INFO,
52+
Text: scanner.Text(),
53+
},
54+
},
55+
})
56+
}
57+
}()
58+
terraform.SetStdout(writer)
3559
err = terraform.Init(ctx)
3660
if err != nil {
37-
return nil, xerrors.Errorf("initialize terraform: %w", err)
61+
return xerrors.Errorf("initialize terraform: %w", err)
3862
}
3963

4064
env := map[string]string{}
@@ -46,26 +70,73 @@ func (t *terraform) Provision(ctx context.Context, request *proto.Provision_Requ
4670
case proto.ParameterDestination_PROVISIONER_VARIABLE:
4771
options = append(options, tfexec.Var(fmt.Sprintf("%s=%s", param.Name, param.Value)))
4872
default:
49-
return nil, xerrors.Errorf("unsupported parameter type %q for %q", param.DestinationScheme, param.Name)
73+
return xerrors.Errorf("unsupported parameter type %q for %q", param.DestinationScheme, param.Name)
5074
}
5175
}
5276
err = terraform.SetEnv(env)
5377
if err != nil {
54-
return nil, xerrors.Errorf("apply environment variables: %w", err)
78+
return xerrors.Errorf("apply environment variables: %w", err)
5579
}
5680

81+
reader, writer = io.Pipe()
82+
defer reader.Close()
83+
defer writer.Close()
84+
go func() {
85+
decoder := json.NewDecoder(reader)
86+
for {
87+
var log terraformProvisionLog
88+
err := decoder.Decode(&log)
89+
if err != nil {
90+
return
91+
}
92+
93+
logLevel, err := convertTerraformLogLevel(log.Level)
94+
if err != nil {
95+
// Not a big deal, but we should handle this at some point!
96+
continue
97+
}
98+
_ = stream.Send(&proto.Provision_Response{
99+
Type: &proto.Provision_Response_Log{
100+
Log: &proto.Log{
101+
Level: logLevel,
102+
Text: log.Message,
103+
},
104+
},
105+
})
106+
107+
if log.Diagnostic == nil {
108+
continue
109+
}
110+
111+
// If the diagnostic is provided, let's provide a bit more info!
112+
logLevel, err = convertTerraformLogLevel(log.Diagnostic.Severity)
113+
if err != nil {
114+
continue
115+
}
116+
_ = stream.Send(&proto.Provision_Response{
117+
Type: &proto.Provision_Response_Log{
118+
Log: &proto.Log{
119+
Level: logLevel,
120+
Text: log.Diagnostic.Detail,
121+
},
122+
},
123+
})
124+
}
125+
}()
126+
127+
terraform.SetStdout(writer)
57128
err = terraform.Apply(ctx, options...)
58129
if err != nil {
59-
return nil, xerrors.Errorf("apply terraform: %w", err)
130+
return xerrors.Errorf("apply terraform: %w", err)
60131
}
61132

62133
statefileContent, err := os.ReadFile(statefilePath)
63134
if err != nil {
64-
return nil, xerrors.Errorf("read file %q: %w", statefilePath, err)
135+
return xerrors.Errorf("read file %q: %w", statefilePath, err)
65136
}
66137
state, err := terraform.ShowStateFile(ctx, statefilePath)
67138
if err != nil {
68-
return nil, xerrors.Errorf("show state file %q: %w", statefilePath, err)
139+
return xerrors.Errorf("show state file %q: %w", statefilePath, err)
69140
}
70141
resources := make([]*proto.Resource, 0)
71142
if state.Values != nil {
@@ -77,8 +148,42 @@ func (t *terraform) Provision(ctx context.Context, request *proto.Provision_Requ
77148
}
78149
}
79150

80-
return &proto.Provision_Response{
81-
Resources: resources,
82-
State: statefileContent,
83-
}, nil
151+
return stream.Send(&proto.Provision_Response{
152+
Type: &proto.Provision_Response_Complete{
153+
Complete: &proto.Provision_Complete{
154+
State: statefileContent,
155+
Resources: resources,
156+
},
157+
},
158+
})
159+
}
160+
161+
type terraformProvisionLog struct {
162+
Level string `json:"@level"`
163+
Message string `json:"@message"`
164+
165+
Diagnostic *terraformProvisionLogDiagnostic `json:"diagnostic"`
166+
}
167+
168+
type terraformProvisionLogDiagnostic struct {
169+
Severity string `json:"severity"`
170+
Summary string `json:"summary"`
171+
Detail string `json:"detail"`
172+
}
173+
174+
func convertTerraformLogLevel(logLevel string) (proto.LogLevel, error) {
175+
switch strings.ToLower(logLevel) {
176+
case "trace":
177+
return proto.LogLevel_TRACE, nil
178+
case "debug":
179+
return proto.LogLevel_DEBUG, nil
180+
case "info":
181+
return proto.LogLevel_INFO, nil
182+
case "warn":
183+
return proto.LogLevel_WARN, nil
184+
case "error":
185+
return proto.LogLevel_ERROR, nil
186+
default:
187+
return proto.LogLevel(0), xerrors.Errorf("invalid log level %q", logLevel)
188+
}
84189
}

0 commit comments

Comments
 (0)