github.com/hashicorp/packer@v1.14.3/post-processor/manifest/post-processor.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 //go:generate packer-sdc mapstructure-to-hcl2 -type Config 5 //go:generate packer-sdc struct-markdown 6 7 package manifest 8 9 import ( 10 "context" 11 "encoding/json" 12 "fmt" 13 "log" 14 "os" 15 "path/filepath" 16 "time" 17 18 "github.com/hashicorp/hcl/v2/hcldec" 19 "github.com/hashicorp/packer-plugin-sdk/common" 20 packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 21 "github.com/hashicorp/packer-plugin-sdk/template/config" 22 "github.com/hashicorp/packer-plugin-sdk/template/interpolate" 23 ) 24 25 type Config struct { 26 common.PackerConfig `mapstructure:",squash"` 27 28 // The manifest will be written to this file. This defaults to 29 // `packer-manifest.json`. 30 OutputPath string `mapstructure:"output"` 31 // Write only filename without the path to the manifest file. This defaults 32 // to false. 33 StripPath bool `mapstructure:"strip_path"` 34 // Don't write the `build_time` field from the output. 35 StripTime bool `mapstructure:"strip_time"` 36 // Arbitrary data to add to the manifest. This is a [template 37 // engine](/packer/docs/templates/legacy_json_templates/engine). Therefore, you 38 // may use user variables and template functions in this field. 39 CustomData map[string]string `mapstructure:"custom_data"` 40 ctx interpolate.Context 41 } 42 43 type PostProcessor struct { 44 config Config 45 } 46 47 type ManifestFile struct { 48 Builds []Artifact `json:"builds"` 49 LastRunUUID string `json:"last_run_uuid"` 50 } 51 52 func (p *PostProcessor) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() } 53 54 func (p *PostProcessor) Configure(raws ...interface{}) error { 55 err := config.Decode(&p.config, &config.DecodeOpts{ 56 PluginType: "packer.post-processor.manifest", 57 Interpolate: true, 58 InterpolateContext: &p.config.ctx, 59 InterpolateFilter: &interpolate.RenderFilter{ 60 Exclude: []string{}, 61 }, 62 }, raws...) 63 if err != nil { 64 return err 65 } 66 67 if p.config.OutputPath == "" { 68 p.config.OutputPath = "packer-manifest.json" 69 } 70 71 if err = interpolate.Validate(p.config.OutputPath, &p.config.ctx); err != nil { 72 return fmt.Errorf("Error parsing target template: %s", err) 73 } 74 75 return nil 76 } 77 78 func (p *PostProcessor) PostProcess(ctx context.Context, ui packersdk.Ui, source packersdk.Artifact) (packersdk.Artifact, bool, bool, error) { 79 generatedData := source.State("generated_data") 80 if generatedData == nil { 81 // Make sure it's not a nil map so we can assign to it later. 82 generatedData = make(map[string]interface{}) 83 } 84 p.config.ctx.Data = generatedData 85 86 for key, data := range p.config.CustomData { 87 interpolatedData, err := createInterpolatedCustomData(&p.config, data) 88 if err != nil { 89 return nil, false, false, err 90 } 91 p.config.CustomData[key] = interpolatedData 92 } 93 94 artifact := &Artifact{} 95 96 var err error 97 var fi os.FileInfo 98 99 // Create the current artifact. 100 for _, name := range source.Files() { 101 af := ArtifactFile{} 102 if fi, err = os.Stat(name); err == nil { 103 af.Size = fi.Size() 104 } 105 if p.config.StripPath { 106 af.Name = filepath.Base(name) 107 } else { 108 af.Name = name 109 } 110 artifact.ArtifactFiles = append(artifact.ArtifactFiles, af) 111 } 112 artifact.ArtifactId = source.Id() 113 artifact.CustomData = p.config.CustomData 114 artifact.BuilderType = p.config.PackerBuilderType 115 artifact.BuildName = p.config.PackerBuildName 116 artifact.BuildTime = time.Now().Unix() 117 if p.config.StripTime { 118 artifact.BuildTime = 0 119 } 120 // Since each post-processor runs in a different process we need a way to 121 // coordinate between various post-processors in a single packer run. We do 122 // this by setting a UUID per run and tracking this in the manifest file. 123 // When we detect that the UUID in the file is the same, we know that we are 124 // part of the same run and we simply add our data to the list. If the UUID 125 // is different we will check the -force flag and decide whether to truncate 126 // the file before we proceed. 127 artifact.PackerRunUUID = os.Getenv("PACKER_RUN_UUID") 128 129 // Create a lock file with exclusive access. If this fails we will retry 130 // after a delay. 131 lockFilename := p.config.OutputPath + ".lock" 132 for i := 0; i < 3; i++ { 133 // The file should not be locked for very long so we'll keep this short. 134 time.Sleep((time.Duration(i) * 200 * time.Millisecond)) 135 _, err = os.OpenFile(lockFilename, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) 136 if err == nil { 137 break 138 } 139 log.Printf("Error locking manifest file for reading and writing. Will sleep and retry. %s", err) 140 } 141 defer os.Remove(lockFilename) 142 143 // Read the current manifest file from disk 144 var contents []byte 145 if contents, err = os.ReadFile(p.config.OutputPath); err != nil && !os.IsNotExist(err) { 146 return source, true, true, fmt.Errorf("Unable to open %s for reading: %s", p.config.OutputPath, err) 147 } 148 149 // Parse the manifest file JSON, if we have one 150 manifestFile := &ManifestFile{} 151 if len(contents) > 0 { 152 if err = json.Unmarshal(contents, manifestFile); err != nil { 153 return source, true, true, fmt.Errorf("Unable to parse content from %s: %s", p.config.OutputPath, err) 154 } 155 } 156 157 // If -force is set and we are not on same run, truncate the file. Otherwise 158 // we will continue to add new builds to the existing manifest file. 159 if p.config.PackerForce && os.Getenv("PACKER_RUN_UUID") != manifestFile.LastRunUUID { 160 manifestFile = &ManifestFile{} 161 } 162 163 // Add the current artifact to the manifest file 164 manifestFile.Builds = append(manifestFile.Builds, *artifact) 165 manifestFile.LastRunUUID = os.Getenv("PACKER_RUN_UUID") 166 167 // Write JSON to disk 168 if out, err := json.MarshalIndent(manifestFile, "", " "); err == nil { 169 if err = os.WriteFile(p.config.OutputPath, out, 0664); err != nil { 170 return source, true, true, fmt.Errorf("Unable to write %s: %s", p.config.OutputPath, err) 171 } 172 } else { 173 return source, true, true, fmt.Errorf("Unable to marshal JSON %s", err) 174 } 175 176 // The manifest should never delete the artifacts it is set to record, so it 177 // forcibly sets "keep" to true. 178 return source, true, true, nil 179 } 180 181 func createInterpolatedCustomData(config *Config, customData string) (string, error) { 182 interpolatedCmd, err := interpolate.Render(customData, &config.ctx) 183 if err != nil { 184 return "", fmt.Errorf("Error interpolating custom data: %s", err) 185 } 186 return interpolatedCmd, nil 187 }