@@ -8,7 +8,10 @@ import (
88 "path/filepath"
99 "regexp"
1010 "strings"
11+ "sync"
12+ "syscall"
1113 "text/template"
14+ "time"
1215
1316 "github.com/aws/aws-sdk-go-v2/aws"
1417 "github.com/hashicorp/terraform-exec/tfexec"
@@ -25,6 +28,40 @@ type Template struct {
2528//go:embed templates/*.tpl
2629var templates embed.FS
2730
31+ var tfPluginMux sync.Mutex
32+
33+ func lockPluginCache (pluginCacheDir string ) (* os.File , error ) {
34+ if err := os .MkdirAll (pluginCacheDir , 0755 ); err != nil {
35+ return nil , err
36+ }
37+
38+ lockPath := filepath .Join (pluginCacheDir , ".terraform-plugin.lock" )
39+ lockFile , err := os .OpenFile (lockPath , os .O_CREATE | os .O_RDWR , 0644 )
40+ if err != nil {
41+ return nil , fmt .Errorf ("failed to open lock file: %w" , err )
42+ }
43+
44+ if err := syscall .Flock (int (lockFile .Fd ()), syscall .LOCK_EX ); err != nil {
45+ _ = lockFile .Close ()
46+ return nil , fmt .Errorf ("failed to acquire exclusive lock: %w" , err )
47+ }
48+
49+ return lockFile , nil
50+ }
51+
52+ func unlockPluginCache (lockFile * os.File ) error {
53+ if lockFile == nil {
54+ return nil
55+ }
56+
57+ if err := syscall .Flock (int (lockFile .Fd ()), syscall .LOCK_UN ); err != nil {
58+ _ = lockFile .Close ()
59+ return fmt .Errorf ("failed to unlock plugin cache: %w" , err )
60+ }
61+
62+ return lockFile .Close ()
63+ }
64+
2865// CreateTerraformFileFromTemplate populates a Terraform template and create files in the state
2966func CreateTerraformFilesFromTemplate (terraformTemplateFilePath string , TerraformOutputFileName string , terraformOutputDir string , templateData any ) error {
3067 template := Template {
@@ -43,7 +80,9 @@ func CreateAdditionalTerraformFiles(tfFiles ...Template) error {
4380 if err != nil {
4481 return err
4582 }
46- defer file .Close ()
83+ defer func () {
84+ _ = file .Close ()
85+ }()
4786
4887 t := template .New (filepath .Base (tfFile .TemplateFilename )).Funcs (template.FuncMap {
4988 "stringReplace" : strings .Replace ,
@@ -96,23 +135,52 @@ func initTerraform(ctx context.Context, workingDir, terraformExecPath string, cr
96135 return nil , err
97136 }
98137
138+ pluginCacheDir := fmt .Sprintf ("%s/plugin-cache" , filepath .Dir (terraformExecPath ))
139+
99140 env := map [string ]string {
100141 "AWS_ACCESS_KEY_ID" : credentials .AccessKeyID ,
101142 "AWS_SECRET_ACCESS_KEY" : credentials .SecretAccessKey ,
102143 "SPOTINST_TOKEN" : os .Getenv ("SPOTINST_TOKEN" ),
103144 "SPOTINST_ACCOUNT" : os .Getenv ("SPOTINST_ACCOUNT" ),
104- "TF_PLUGIN_CACHE_DIR" : fmt . Sprintf ( "%s/plugin-cache" , filepath . Dir ( terraformExecPath )) ,
145+ "TF_PLUGIN_CACHE_DIR" : pluginCacheDir ,
105146 }
106147
107- // this overrides all ENVVARs that are passed to Terraform
108148 err = tf .SetEnv (env )
109149 if err != nil {
110150 return nil , err
111151 }
112152
113- err = tf .Init (ctx , tfexec .Upgrade (true ))
153+ tfPluginMux .Lock ()
154+ defer tfPluginMux .Unlock ()
155+
156+ lockFile , err := lockPluginCache (pluginCacheDir )
114157 if err != nil {
115- return nil , err
158+ return nil , fmt .Errorf ("failed to acquire plugin cache lock: %w" , err )
159+ }
160+ defer func () {
161+ time .Sleep (500 * time .Millisecond )
162+ _ = unlockPluginCache (lockFile )
163+ }()
164+
165+ var initErr error
166+ maxRetries := 3
167+ for i := 0 ; i < maxRetries ; i ++ {
168+ initErr = tf .Init (ctx , tfexec .Upgrade (true ))
169+ if initErr == nil {
170+ break
171+ }
172+
173+ if strings .Contains (initErr .Error (), "text file busy" ) && i < maxRetries - 1 {
174+ waitTime := time .Duration (i + 1 ) * 2 * time .Second
175+ time .Sleep (waitTime )
176+ continue
177+ }
178+
179+ break
180+ }
181+
182+ if initErr != nil {
183+ return nil , initErr
116184 }
117185
118186 return tf , nil
@@ -127,12 +195,24 @@ func ApplyTerraform(ctx context.Context, workingDir, terraformExecPath string, c
127195 return err
128196 }
129197
130- err = tf .Apply (ctx )
131- if err != nil {
132- return err
198+ var applyErr error
199+ maxRetries := 5
200+ for i := 0 ; i < maxRetries ; i ++ {
201+ applyErr = tf .Apply (ctx )
202+ if applyErr == nil {
203+ return nil
204+ }
205+
206+ if strings .Contains (applyErr .Error (), "text file busy" ) && i < maxRetries - 1 {
207+ waitTime := time .Duration (i + 1 ) * time .Second
208+ time .Sleep (waitTime )
209+ continue
210+ }
211+
212+ break
133213 }
134214
135- return nil
215+ return applyErr
136216}
137217
138218// PlanTerraform just applies the already created terraform files
@@ -150,12 +230,24 @@ func PlanTerraform(ctx context.Context, workingDir, terraformExecPath string, cr
150230 return err
151231 }
152232
153- _ , err = tf .Plan (ctx , tfexec .Out (workingDir + "/plan.out" ))
154- if err != nil {
155- return err
233+ var planErr error
234+ maxRetries := 5
235+ for i := 0 ; i < maxRetries ; i ++ {
236+ _ , planErr = tf .Plan (ctx , tfexec .Out (workingDir + "/plan.out" ))
237+ if planErr == nil {
238+ return nil
239+ }
240+
241+ if strings .Contains (planErr .Error (), "text file busy" ) && i < maxRetries - 1 {
242+ waitTime := time .Duration (i + 1 ) * time .Second
243+ time .Sleep (waitTime )
244+ continue
245+ }
246+
247+ break
156248 }
157249
158- return nil
250+ return planErr
159251}
160252
161253func DestroyTerraform (ctx context.Context , workingDir , terraformExecPath string , credentials aws.Credentials ) error {
@@ -164,20 +256,34 @@ func DestroyTerraform(ctx context.Context, workingDir, terraformExecPath string,
164256 return err
165257 }
166258
167- err = tf .Destroy (ctx )
168- if err != nil {
169- return err
259+ var destroyErr error
260+ maxRetries := 5
261+ for i := 0 ; i < maxRetries ; i ++ {
262+ destroyErr = tf .Destroy (ctx )
263+ if destroyErr == nil {
264+ return nil
265+ }
266+
267+ if strings .Contains (destroyErr .Error (), "text file busy" ) && i < maxRetries - 1 {
268+ waitTime := time .Duration (i + 1 ) * time .Second
269+ time .Sleep (waitTime )
270+ continue
271+ }
272+
273+ break
170274 }
171275
172- return nil
276+ return destroyErr
173277}
174278
175279func CleanupTerraformDirectory (dir string ) error {
176280 d , err := os .Open (dir )
177281 if err != nil {
178282 return err
179283 }
180- defer d .Close ()
284+ defer func () {
285+ _ = d .Close ()
286+ }()
181287 names , err := d .Readdirnames (- 1 )
182288 if err != nil {
183289 return err
0 commit comments