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  }