github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/builtin/provisioners/file/resource_provisioner.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package file 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "io/ioutil" 11 "os" 12 13 "github.com/mitchellh/go-homedir" 14 "github.com/terramate-io/tf/communicator" 15 "github.com/terramate-io/tf/configs/configschema" 16 "github.com/terramate-io/tf/provisioners" 17 "github.com/terramate-io/tf/tfdiags" 18 "github.com/zclconf/go-cty/cty" 19 ) 20 21 func New() provisioners.Interface { 22 ctx, cancel := context.WithCancel(context.Background()) 23 return &provisioner{ 24 ctx: ctx, 25 cancel: cancel, 26 } 27 } 28 29 type provisioner struct { 30 // We store a context here tied to the lifetime of the provisioner. 31 // This allows the Stop method to cancel any in-flight requests. 32 ctx context.Context 33 cancel context.CancelFunc 34 } 35 36 func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) { 37 schema := &configschema.Block{ 38 Attributes: map[string]*configschema.Attribute{ 39 "source": { 40 Type: cty.String, 41 Optional: true, 42 }, 43 44 "content": { 45 Type: cty.String, 46 Optional: true, 47 }, 48 49 "destination": { 50 Type: cty.String, 51 Required: true, 52 }, 53 }, 54 } 55 resp.Provisioner = schema 56 return resp 57 } 58 59 func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) { 60 cfg, err := p.GetSchema().Provisioner.CoerceValue(req.Config) 61 if err != nil { 62 resp.Diagnostics = resp.Diagnostics.Append(err) 63 } 64 65 source := cfg.GetAttr("source") 66 content := cfg.GetAttr("content") 67 68 switch { 69 case !source.IsNull() && !content.IsNull(): 70 resp.Diagnostics = resp.Diagnostics.Append(errors.New("Cannot set both 'source' and 'content'")) 71 return resp 72 case source.IsNull() && content.IsNull(): 73 resp.Diagnostics = resp.Diagnostics.Append(errors.New("Must provide one of 'source' or 'content'")) 74 return resp 75 } 76 77 return resp 78 } 79 80 func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { 81 if req.Connection.IsNull() { 82 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 83 tfdiags.Error, 84 "file provisioner error", 85 "Missing connection configuration for provisioner.", 86 )) 87 return resp 88 } 89 90 comm, err := communicator.New(req.Connection) 91 if err != nil { 92 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 93 tfdiags.Error, 94 "file provisioner error", 95 err.Error(), 96 )) 97 return resp 98 } 99 100 // Get the source 101 src, deleteSource, err := getSrc(req.Config) 102 if err != nil { 103 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 104 tfdiags.Error, 105 "file provisioner error", 106 err.Error(), 107 )) 108 return resp 109 } 110 if deleteSource { 111 defer os.Remove(src) 112 } 113 114 // Begin the file copy 115 dst := req.Config.GetAttr("destination").AsString() 116 if err := copyFiles(p.ctx, comm, src, dst); err != nil { 117 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 118 tfdiags.Error, 119 "file provisioner error", 120 err.Error(), 121 )) 122 return resp 123 } 124 125 return resp 126 } 127 128 // getSrc returns the file to use as source 129 func getSrc(v cty.Value) (string, bool, error) { 130 content := v.GetAttr("content") 131 src := v.GetAttr("source") 132 133 switch { 134 case !content.IsNull(): 135 file, err := ioutil.TempFile("", "tf-file-content") 136 if err != nil { 137 return "", true, err 138 } 139 140 if _, err = file.WriteString(content.AsString()); err != nil { 141 return "", true, err 142 } 143 144 return file.Name(), true, nil 145 146 case !src.IsNull(): 147 expansion, err := homedir.Expand(src.AsString()) 148 return expansion, false, err 149 150 default: 151 panic("source and content cannot both be null") 152 } 153 } 154 155 // copyFiles is used to copy the files from a source to a destination 156 func copyFiles(ctx context.Context, comm communicator.Communicator, src, dst string) error { 157 retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout()) 158 defer cancel() 159 160 // Wait and retry until we establish the connection 161 err := communicator.Retry(retryCtx, func() error { 162 return comm.Connect(nil) 163 }) 164 if err != nil { 165 return err 166 } 167 168 // disconnect when the context is canceled, which will close this after 169 // Apply as well. 170 go func() { 171 <-ctx.Done() 172 comm.Disconnect() 173 }() 174 175 info, err := os.Stat(src) 176 if err != nil { 177 return err 178 } 179 180 // If we're uploading a directory, short circuit and do that 181 if info.IsDir() { 182 if err := comm.UploadDir(dst, src); err != nil { 183 return fmt.Errorf("Upload failed: %v", err) 184 } 185 return nil 186 } 187 188 // We're uploading a file... 189 f, err := os.Open(src) 190 if err != nil { 191 return err 192 } 193 defer f.Close() 194 195 err = comm.Upload(dst, f) 196 if err != nil { 197 return fmt.Errorf("Upload failed: %v", err) 198 } 199 200 return err 201 } 202 203 func (p *provisioner) Stop() error { 204 p.cancel() 205 return nil 206 } 207 208 func (p *provisioner) Close() error { 209 return nil 210 }