1
1
package terraform
2
2
3
3
import (
4
- "context"
4
+ "bufio"
5
+ "encoding/json"
5
6
"fmt"
7
+ "io"
6
8
"os"
7
9
"path/filepath"
10
+ "strings"
8
11
9
12
"github.com/hashicorp/terraform-exec/tfexec"
10
13
"golang.org/x/xerrors"
@@ -13,28 +16,49 @@ import (
13
16
)
14
17
15
18
// 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 ()
17
22
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
+ }
21
28
}
22
29
23
30
terraform , err := tfexec .NewTerraform (request .Directory , t .binaryPath )
24
31
if err != nil {
25
- return nil , xerrors .Errorf ("create new terraform executor: %w" , err )
32
+ return xerrors .Errorf ("create new terraform executor: %w" , err )
26
33
}
27
34
version , _ , err := terraform .Version (ctx , false )
28
35
if err != nil {
29
- return nil , xerrors .Errorf ("get terraform version: %w" , err )
36
+ return xerrors .Errorf ("get terraform version: %w" , err )
30
37
}
31
38
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 ())
33
40
}
34
41
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 )
35
59
err = terraform .Init (ctx )
36
60
if err != nil {
37
- return nil , xerrors .Errorf ("initialize terraform: %w" , err )
61
+ return xerrors .Errorf ("initialize terraform: %w" , err )
38
62
}
39
63
40
64
env := map [string ]string {}
@@ -46,26 +70,73 @@ func (t *terraform) Provision(ctx context.Context, request *proto.Provision_Requ
46
70
case proto .ParameterDestination_PROVISIONER_VARIABLE :
47
71
options = append (options , tfexec .Var (fmt .Sprintf ("%s=%s" , param .Name , param .Value )))
48
72
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 )
50
74
}
51
75
}
52
76
err = terraform .SetEnv (env )
53
77
if err != nil {
54
- return nil , xerrors .Errorf ("apply environment variables: %w" , err )
78
+ return xerrors .Errorf ("apply environment variables: %w" , err )
55
79
}
56
80
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 )
57
128
err = terraform .Apply (ctx , options ... )
58
129
if err != nil {
59
- return nil , xerrors .Errorf ("apply terraform: %w" , err )
130
+ return xerrors .Errorf ("apply terraform: %w" , err )
60
131
}
61
132
62
133
statefileContent , err := os .ReadFile (statefilePath )
63
134
if err != nil {
64
- return nil , xerrors .Errorf ("read file %q: %w" , statefilePath , err )
135
+ return xerrors .Errorf ("read file %q: %w" , statefilePath , err )
65
136
}
66
137
state , err := terraform .ShowStateFile (ctx , statefilePath )
67
138
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 )
69
140
}
70
141
resources := make ([]* proto.Resource , 0 )
71
142
if state .Values != nil {
@@ -77,8 +148,42 @@ func (t *terraform) Provision(ctx context.Context, request *proto.Provision_Requ
77
148
}
78
149
}
79
150
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
+ }
84
189
}
0 commit comments