github.com/hashicorp/packer@v1.14.3/packer/provisioner.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package packer 5 6 import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "log" 11 "os" 12 13 hcpSbomProvisioner "github.com/hashicorp/packer/provisioner/hcp-sbom" 14 15 hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models" 16 "github.com/klauspost/compress/zstd" 17 18 "time" 19 20 "github.com/hashicorp/hcl/v2/hcldec" 21 packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 22 "github.com/hashicorp/packer-plugin-sdk/packerbuilderdata" 23 ) 24 25 // A HookedProvisioner represents a provisioner and information describing it 26 type HookedProvisioner struct { 27 Provisioner packersdk.Provisioner 28 Config interface{} 29 TypeName string 30 } 31 32 // A Hook implementation that runs the given provisioners. 33 type ProvisionHook struct { 34 // The provisioners to run as part of the hook. These should already 35 // be prepared (by calling Prepare) at some earlier stage. 36 Provisioners []*HookedProvisioner 37 } 38 39 // BuilderDataCommonKeys is the list of common keys that all builder will 40 // return 41 var BuilderDataCommonKeys = []string{ 42 "ID", 43 // The following correspond to communicator-agnostic functions that are } 44 // part of the SSH and WinRM communicator implementations. These functions 45 // are not part of the communicator interface, but are stored on the 46 // Communicator Config and return the appropriate values rather than 47 // depending on the actual communicator config values. E.g "Password" 48 // reprosents either WinRMPassword or SSHPassword, which makes this more 49 // useful if a template contains multiple builds. 50 "Host", 51 "Port", 52 "User", 53 "Password", 54 "ConnType", 55 "PackerRunUUID", 56 "PackerHTTPPort", 57 "PackerHTTPIP", 58 "PackerHTTPAddr", 59 "SSHPublicKey", 60 "SSHPrivateKey", 61 "WinRMPassword", 62 } 63 64 // Provisioners interpolate most of their fields in the prepare stage; this 65 // placeholder map helps keep fields that are only generated at build time from 66 // accidentally being interpolated into empty strings at prepare time. 67 // This helper function generates the most basic placeholder data which should 68 // be accessible to the provisioners. It is used to initialize provisioners, to 69 // force validation using the `generated` template function. In the future, 70 // custom generated data could be passed into provisioners from builders to 71 // enable specialized builder-specific (but still validated!!) access to builder 72 // data. 73 func BasicPlaceholderData() map[string]string { 74 placeholderData := map[string]string{} 75 for _, key := range BuilderDataCommonKeys { 76 placeholderData[key] = fmt.Sprintf("Build_%s. "+packerbuilderdata.PlaceholderMsg, key) 77 } 78 79 // Backwards-compatability: WinRM Password can get through without forcing 80 // the generated func validation. 81 placeholderData["WinRMPassword"] = "{{.WinRMPassword}}" 82 83 return placeholderData 84 } 85 86 func CastDataToMap(data interface{}) map[string]interface{} { 87 88 if interMap, ok := data.(map[string]interface{}); ok { 89 // null and file builder sometimes don't use a communicator and 90 // therefore don't go through RPC 91 return interMap 92 } 93 94 // Provisioners expect a map[string]interface{} in their data field, but 95 // it gets converted into a map[interface]interface on the way over the 96 // RPC. Check that data can be cast into such a form, and cast it. 97 cast := make(map[string]interface{}) 98 interMap, ok := data.(map[interface{}]interface{}) 99 if !ok { 100 log.Printf("Unable to read map[string]interface out of data."+ 101 "Using empty interface: %#v", data) 102 } else { 103 for key, val := range interMap { 104 keyString, ok := key.(string) 105 if ok { 106 cast[keyString] = val 107 } else { 108 log.Printf("Error casting generated data key to a string.") 109 } 110 } 111 } 112 return cast 113 } 114 115 // Runs the provisioners in order. 116 func (h *ProvisionHook) Run(ctx context.Context, name string, ui packersdk.Ui, comm packersdk.Communicator, data interface{}) error { 117 // Shortcut 118 if len(h.Provisioners) == 0 { 119 return nil 120 } 121 122 if comm == nil { 123 return fmt.Errorf( 124 "No communicator found for provisioners! This is usually because the\n" + 125 "`communicator` config was set to \"none\". If you have any provisioners\n" + 126 "then a communicator is required. Please fix this to continue.") 127 } 128 for _, p := range h.Provisioners { 129 ts := CheckpointReporter.AddSpan(p.TypeName, "provisioner", p.Config) 130 131 cast := CastDataToMap(data) 132 err := p.Provisioner.Provision(ctx, ui, comm, cast) 133 134 ts.End(err) 135 if err != nil { 136 return err 137 } 138 } 139 140 return nil 141 } 142 143 // PausedProvisioner is a Provisioner implementation that pauses before 144 // the provisioner is actually run. 145 type PausedProvisioner struct { 146 PauseBefore time.Duration 147 Provisioner packersdk.Provisioner 148 } 149 150 func (p *PausedProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() } 151 func (p *PausedProvisioner) FlatConfig() interface{} { return p.FlatConfig() } 152 func (p *PausedProvisioner) Prepare(raws ...interface{}) error { 153 return p.Provisioner.Prepare(raws...) 154 } 155 156 func (p *PausedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error { 157 158 // Use a select to determine if we get cancelled during the wait 159 ui.Say(fmt.Sprintf("Pausing %s before the next provisioner...", p.PauseBefore)) 160 select { 161 case <-time.After(p.PauseBefore): 162 case <-ctx.Done(): 163 return ctx.Err() 164 } 165 166 return p.Provisioner.Provision(ctx, ui, comm, generatedData) 167 } 168 169 // RetriedProvisioner is a Provisioner implementation that retries 170 // the provisioner whenever there's an error. 171 type RetriedProvisioner struct { 172 MaxRetries int 173 Provisioner packersdk.Provisioner 174 } 175 176 func (r *RetriedProvisioner) ConfigSpec() hcldec.ObjectSpec { return r.ConfigSpec() } 177 func (r *RetriedProvisioner) FlatConfig() interface{} { return r.FlatConfig() } 178 func (r *RetriedProvisioner) Prepare(raws ...interface{}) error { 179 return r.Provisioner.Prepare(raws...) 180 } 181 182 func (r *RetriedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error { 183 if ctx.Err() != nil { // context was cancelled 184 return ctx.Err() 185 } 186 187 err := r.Provisioner.Provision(ctx, ui, comm, generatedData) 188 if err == nil { 189 return nil 190 } 191 192 leftTries := r.MaxRetries 193 for ; leftTries > 0; leftTries-- { 194 if ctx.Err() != nil { // context was cancelled 195 return ctx.Err() 196 } 197 198 ui.Say(fmt.Sprintf("Provisioner failed with %q, retrying with %d trie(s) left", err, leftTries)) 199 200 err := r.Provisioner.Provision(ctx, ui, comm, generatedData) 201 if err == nil { 202 return nil 203 } 204 205 } 206 ui.Say("retry limit reached.") 207 208 return err 209 } 210 211 // DebuggedProvisioner is a Provisioner implementation that waits until a key 212 // press before the provisioner is actually run. 213 type DebuggedProvisioner struct { 214 Provisioner packersdk.Provisioner 215 } 216 217 func (p *DebuggedProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() } 218 func (p *DebuggedProvisioner) FlatConfig() interface{} { return p.FlatConfig() } 219 func (p *DebuggedProvisioner) Prepare(raws ...interface{}) error { 220 return p.Provisioner.Prepare(raws...) 221 } 222 223 func (p *DebuggedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error { 224 // Use a select to determine if we get cancelled during the wait 225 message := "Pausing before the next provisioner . Press enter to continue." 226 227 result := make(chan string, 1) 228 go func() { 229 line, err := ui.Ask(message) 230 if err != nil { 231 log.Printf("Error asking for input: %s", err) 232 } 233 234 result <- line 235 }() 236 237 select { 238 case <-result: 239 case <-ctx.Done(): 240 return ctx.Err() 241 } 242 243 return p.Provisioner.Provision(ctx, ui, comm, generatedData) 244 } 245 246 // SBOMInternalProvisioner is a wrapper provisioner for the `hcp-sbom` provisioner 247 // that sets the path for SBOM file download and, after the successful execution of 248 // the `hcp-sbom` provisioner, compresses the SBOM and prepares the data for API 249 // integration. 250 type SBOMInternalProvisioner struct { 251 Provisioner packersdk.Provisioner 252 CompressedData []byte 253 SBOMFormat hcpPackerModels.HashicorpCloudPacker20230101SbomFormat 254 SBOMName string 255 } 256 257 func (p *SBOMInternalProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() } 258 func (p *SBOMInternalProvisioner) FlatConfig() interface{} { return p.FlatConfig() } 259 func (p *SBOMInternalProvisioner) Prepare(raws ...interface{}) error { 260 return p.Provisioner.Prepare(raws...) 261 } 262 263 func (p *SBOMInternalProvisioner) Provision( 264 ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, 265 generatedData map[string]interface{}, 266 ) error { 267 cwd, err := os.Getwd() 268 if err != nil { 269 return fmt.Errorf("failed to get current working directory for Packer SBOM: %s", err) 270 } 271 272 tmpFile, err := os.CreateTemp(cwd, "packer-sbom-*.json") 273 if err != nil { 274 return fmt.Errorf("failed to create internal temporary file for Packer SBOM: %s", err) 275 } 276 277 tmpFileName := tmpFile.Name() 278 if err = tmpFile.Close(); err != nil { 279 return fmt.Errorf("failed to close temporary file for Packer SBOM %s: %s", tmpFileName, err) 280 } 281 282 defer func(name string) { 283 fileRemoveErr := os.Remove(name) 284 if fileRemoveErr != nil { 285 log.Printf("Error removing SBOM temporary file %s: %s", name, fileRemoveErr) 286 } 287 }(tmpFile.Name()) 288 289 generatedData["dst"] = tmpFile.Name() 290 291 err = p.Provisioner.Provision(ctx, ui, comm, generatedData) 292 if err != nil { 293 return err 294 } 295 296 packerSbom, err := os.Open(tmpFileName) 297 if err != nil { 298 return fmt.Errorf("failed to open Packer SBOM file %q: %s", tmpFileName, err) 299 } 300 301 provisionerOut := &hcpSbomProvisioner.PackerSBOM{} 302 err = json.NewDecoder(packerSbom).Decode(provisionerOut) 303 if err != nil { 304 return fmt.Errorf("malformed packer SBOM output from file %q: %s", tmpFileName, err) 305 } 306 307 encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBestCompression)) 308 if err != nil { 309 return fmt.Errorf("failed to create zstd encoder: %s", err) 310 } 311 p.CompressedData = encoder.EncodeAll(provisionerOut.RawSBOM, nil) 312 p.SBOMFormat = provisionerOut.Format 313 p.SBOMName = provisionerOut.Name 314 315 return nil 316 }