@@ -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 (200 * 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
@@ -177,7 +245,9 @@ func CleanupTerraformDirectory(dir string) error {
177245 if err != nil {
178246 return err
179247 }
180- defer d .Close ()
248+ defer func () {
249+ _ = d .Close ()
250+ }()
181251 names , err := d .Readdirnames (- 1 )
182252 if err != nil {
183253 return err
0 commit comments