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