Files
Packer-Cn/packer/provisioner.go
HariandHari Om eee3805c06 Feature/enforced provisioner (#13591)
* added the parser for the enforced block

* Enhance enforced provisioner parsing to support HCL and JSON formats

- Updated ParseProvisionerBlocks to handle both HCL and JSON syntax, including legacy JSON format.

- Added comprehensive test cases for JSON provisioner parsing.

- Improved ExtractBuildProvisionerHCL to merge inline commands from shell provisioners.

- Enhanced logging for enforced block operations in HCP Packer.

* Remove PublishEnforcedBlocks function from Bucket struct

* Remove ExtractBuildProvisionerHCL function and unused imports

* Reverted the version upgrade

* Added the internal-sdk for the enforcedProvsioner api changes

* Enhance enforced provisioner handling and error reporting

- Update error handling in FetchEnforcedBlocks to return detailed errors instead of warnings.
- Modify GetCoreBuildProvisionerFromBlock to accept build name for overrides.
- Add tests for FetchEnforcedBlocks to ensure correct behavior and error handling.
- Implement diagnostics for unsupported legacy JSON templates.

* Implement enforced provisioner parsing and handling

- Introduced a new package `enforcedparser` to handle parsing of enforced provisioner blocks from HCL and JSON formats.

- Refactored existing code to utilize the new `ParseProvisionerBlocks` function from the `enforcedparser` package.

- Updated `GetCoreBuildProvisionerFromEnforcedBlock` method to convert enforced provisioner blocks into core build provisioners.

- Enhanced error handling and logging during the parsing process.

- Added tests for the new parsing functionality and ensured existing tests were updated to reflect changes.

- Modified `InjectEnforcedProvisioners` method in JSON registry to utilize the new parsing logic.

* Add test case for -skip-enforcement flag in BuildArgs

* Refactor sensitive variable handling in provisioners and add related tests

* Refactor enforced provisioner handling: remove internal parser, update tests, and streamline API interactions

* Enhance provisioner block parsing: add error handling for invalid combinations and expand test coverage

* Remove internal SDK replacement for enforced block types in go.mod

* Update dependencies in go.mod and go.sum: bump hcp-sdk-go and packer-plugin-sdk versions, adjust syft version, and update OpenTelemetry packages

* Update hcp-sdk-go dependency to v0.172.0 in go.mod and go.sum

* Fix formatting in TestBuildCommand_ParseArgs and add newline at end of json_enforced_test.go

* Refactor testJSONRegistryWithBuilds: remove environment variable setup and streamline registry initialization

* Rename injected variable for clarity in InjectEnforcedProvisioners function

---------

Co-authored-by: Hari Om <[email protected]>
2026-04-17 14:17:28 +05:30

366 lines
11 KiB
Go

// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package packer
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"time"
hcpSbomProvisioner "github.com/hashicorp/packer/provisioner/hcp-sbom"
hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
"github.com/klauspost/compress/zstd"
"github.com/hashicorp/hcl/v2/hcldec"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/packerbuilderdata"
)
// A HookedProvisioner represents a provisioner and information describing it
type HookedProvisioner struct {
Provisioner packersdk.Provisioner
Config interface{}
TypeName string
}
// A Hook implementation that runs the given provisioners.
type ProvisionHook struct {
// The provisioners to run as part of the hook. These should already
// be prepared (by calling Prepare) at some earlier stage.
Provisioners []*HookedProvisioner
}
// BuilderDataCommonKeys is the list of common keys that all builder will
// return
var BuilderDataCommonKeys = []string{
"ID",
// The following correspond to communicator-agnostic functions that are }
// part of the SSH and WinRM communicator implementations. These functions
// are not part of the communicator interface, but are stored on the
// Communicator Config and return the appropriate values rather than
// depending on the actual communicator config values. E.g "Password"
// reprosents either WinRMPassword or SSHPassword, which makes this more
// useful if a template contains multiple builds.
"Host",
"Port",
"User",
"Password",
"ConnType",
"PackerRunUUID",
"PackerHTTPPort",
"PackerHTTPIP",
"PackerHTTPAddr",
"SSHPublicKey",
"SSHPrivateKey",
"WinRMPassword",
}
// Provisioners interpolate most of their fields in the prepare stage; this
// placeholder map helps keep fields that are only generated at build time from
// accidentally being interpolated into empty strings at prepare time.
// This helper function generates the most basic placeholder data which should
// be accessible to the provisioners. It is used to initialize provisioners, to
// force validation using the `generated` template function. In the future,
// custom generated data could be passed into provisioners from builders to
// enable specialized builder-specific (but still validated!!) access to builder
// data.
func BasicPlaceholderData() map[string]string {
placeholderData := map[string]string{}
for _, key := range BuilderDataCommonKeys {
placeholderData[key] = fmt.Sprintf("Build_%s. "+packerbuilderdata.PlaceholderMsg, key)
}
// Backwards-compatability: WinRM Password can get through without forcing
// the generated func validation.
placeholderData["WinRMPassword"] = "{{.WinRMPassword}}"
return placeholderData
}
func CastDataToMap(data interface{}) map[string]interface{} {
if interMap, ok := data.(map[string]interface{}); ok {
// null and file builder sometimes don't use a communicator and
// therefore don't go through RPC
return interMap
}
// Provisioners expect a map[string]interface{} in their data field, but
// it gets converted into a map[interface]interface on the way over the
// RPC. Check that data can be cast into such a form, and cast it.
cast := make(map[string]interface{})
interMap, ok := data.(map[interface{}]interface{})
if !ok {
log.Printf("Unable to read map[string]interface out of data."+
"Using empty interface: %#v", data)
} else {
for key, val := range interMap {
keyString, ok := key.(string)
if ok {
cast[keyString] = val
} else {
log.Printf("Error casting generated data key to a string.")
}
}
}
return cast
}
// Runs the provisioners in order.
func (h *ProvisionHook) Run(ctx context.Context, name string, ui packersdk.Ui, comm packersdk.Communicator, data interface{}) error {
// Shortcut
if len(h.Provisioners) == 0 {
return nil
}
if comm == nil {
return fmt.Errorf(
"No communicator found for provisioners! This is usually because the\n" +
"`communicator` config was set to \"none\". If you have any provisioners\n" +
"then a communicator is required. Please fix this to continue.")
}
for _, p := range h.Provisioners {
ts := CheckpointReporter.AddSpan(p.TypeName, "provisioner", p.Config)
cast := CastDataToMap(data)
err := p.Provisioner.Provision(ctx, ui, comm, cast)
ts.End(err)
if err != nil {
return err
}
}
return nil
}
// ProvisionerWrapOptions contains options for wrapping a provisioner with
// additional behavior like pausing, timeouts, and retries.
type ProvisionerWrapOptions struct {
PauseBefore time.Duration
Timeout time.Duration
MaxRetries int
}
// WrapProvisionerWithOptions wraps a provisioner with additional behavior
// based on the provided options.
func WrapProvisionerWithOptions(provisioner packersdk.Provisioner, opts ProvisionerWrapOptions) packersdk.Provisioner {
wrapped := provisioner
if opts.PauseBefore != 0 {
wrapped = &PausedProvisioner{
PauseBefore: opts.PauseBefore,
Provisioner: wrapped,
}
}
if opts.Timeout != 0 {
wrapped = &TimeoutProvisioner{
Timeout: opts.Timeout,
Provisioner: wrapped,
}
}
if opts.MaxRetries != 0 {
wrapped = &RetriedProvisioner{
MaxRetries: opts.MaxRetries,
Provisioner: wrapped,
}
}
return wrapped
}
// PausedProvisioner is a Provisioner implementation that pauses before
// the provisioner is actually run.
type PausedProvisioner struct {
PauseBefore time.Duration
Provisioner packersdk.Provisioner
}
func (p *PausedProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() }
func (p *PausedProvisioner) FlatConfig() interface{} { return p.FlatConfig() }
func (p *PausedProvisioner) Prepare(raws ...interface{}) error {
return p.Provisioner.Prepare(raws...)
}
func (p *PausedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error {
// Use a select to determine if we get cancelled during the wait
ui.Say(fmt.Sprintf("Pausing %s before the next provisioner...", p.PauseBefore))
select {
case <-time.After(p.PauseBefore):
case <-ctx.Done():
return ctx.Err()
}
return p.Provisioner.Provision(ctx, ui, comm, generatedData)
}
// RetriedProvisioner is a Provisioner implementation that retries
// the provisioner whenever there's an error.
type RetriedProvisioner struct {
MaxRetries int
Provisioner packersdk.Provisioner
}
func (r *RetriedProvisioner) ConfigSpec() hcldec.ObjectSpec { return r.ConfigSpec() }
func (r *RetriedProvisioner) FlatConfig() interface{} { return r.FlatConfig() }
func (r *RetriedProvisioner) Prepare(raws ...interface{}) error {
return r.Provisioner.Prepare(raws...)
}
func (r *RetriedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error {
if ctx.Err() != nil { // context was cancelled
return ctx.Err()
}
err := r.Provisioner.Provision(ctx, ui, comm, generatedData)
if err == nil {
return nil
}
leftTries := r.MaxRetries
for ; leftTries > 0; leftTries-- {
if ctx.Err() != nil { // context was cancelled
return ctx.Err()
}
ui.Say(fmt.Sprintf("Provisioner failed with %q, retrying with %d trie(s) left", err, leftTries))
err := r.Provisioner.Provision(ctx, ui, comm, generatedData)
if err == nil {
return nil
}
}
ui.Say("retry limit reached.")
return err
}
// DebuggedProvisioner is a Provisioner implementation that waits until a key
// press before the provisioner is actually run.
type DebuggedProvisioner struct {
Provisioner packersdk.Provisioner
}
func (p *DebuggedProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() }
func (p *DebuggedProvisioner) FlatConfig() interface{} { return p.FlatConfig() }
func (p *DebuggedProvisioner) Prepare(raws ...interface{}) error {
return p.Provisioner.Prepare(raws...)
}
func (p *DebuggedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error {
// Use a select to determine if we get cancelled during the wait
message := "Pausing before the next provisioner . Press enter to continue."
result := make(chan string, 1)
go func() {
line, err := ui.Ask(message)
if err != nil {
log.Printf("Error asking for input: %s", err)
}
result <- line
}()
select {
case <-result:
case <-ctx.Done():
return ctx.Err()
}
return p.Provisioner.Provision(ctx, ui, comm, generatedData)
}
// SBOMInternalProvisioner is a wrapper provisioner for the `hcp-sbom` provisioner
// that sets the path for SBOM file download and, after the successful execution of
// the `hcp-sbom` provisioner, compresses the SBOM and prepares the data for API
// integration.
type SBOMInternalProvisioner struct {
Provisioner packersdk.Provisioner
CompressedData []byte
SBOMFormat hcpPackerModels.HashicorpCloudPacker20230101SbomFormat
SBOMName string
}
func (p *SBOMInternalProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.Provisioner.ConfigSpec() }
func (p *SBOMInternalProvisioner) FlatConfig() interface{} {
// Try to delegate to inner provisioner if it implements FlatConfig
if fc, ok := p.Provisioner.(interface{ FlatConfig() interface{} }); ok {
return fc.FlatConfig()
}
return nil
}
func (p *SBOMInternalProvisioner) Prepare(raws ...interface{}) error {
return p.Provisioner.Prepare(raws...)
}
func (p *SBOMInternalProvisioner) Provision(
ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator,
generatedData map[string]interface{},
) error {
// Original implementation - all logic now in hcp-sbom provisioner
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current working directory for Packer SBOM: %s", err)
}
tmpFile, err := os.CreateTemp(cwd, "packer-sbom-*.json")
if err != nil {
return fmt.Errorf("failed to create internal temporary file for Packer SBOM: %s", err)
}
tmpFileName := tmpFile.Name()
if err = tmpFile.Close(); err != nil {
return fmt.Errorf("failed to close temporary file for Packer SBOM %s: %s", tmpFileName, err)
}
defer func(name string) {
fileRemoveErr := os.Remove(name)
if fileRemoveErr != nil {
log.Printf("Error removing SBOM temporary file %s: %s", name, fileRemoveErr)
}
}(tmpFile.Name())
generatedData["dst"] = tmpFile.Name()
err = p.Provisioner.Provision(ctx, ui, comm, generatedData)
if err != nil {
return err
}
packerSbom, err := os.Open(tmpFileName)
if err != nil {
return fmt.Errorf("failed to open Packer SBOM file %q: %s", tmpFileName, err)
}
defer func() {
if err := packerSbom.Close(); err != nil {
log.Printf("[WARN] Failed to close Packer SBOM file: %s", err)
}
}()
provisionerOut := &hcpSbomProvisioner.PackerSBOM{}
err = json.NewDecoder(packerSbom).Decode(provisionerOut)
if err != nil {
return fmt.Errorf("malformed packer SBOM output from file %q: %s", tmpFileName, err)
}
encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
if err != nil {
return fmt.Errorf("failed to create zstd encoder: %s", err)
}
p.CompressedData = encoder.EncodeAll(provisionerOut.RawSBOM, nil)
p.SBOMFormat = provisionerOut.Format
p.SBOMName = provisionerOut.Name
return nil
}