mirror of
https://github.com/hashicorp/packer.git
synced 2026-05-28 04:35:38 -04:00
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.
This commit is contained in:
parent
e9230b0b64
commit
cb9b14bb5b
6 changed files with 299 additions and 50 deletions
|
|
@ -4,7 +4,9 @@
|
|||
package hcl2template
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
|
|
@ -19,14 +21,103 @@ var enforcedProvisionerSchema = &hcl.BodySchema{
|
|||
},
|
||||
}
|
||||
|
||||
// ParseProvisionerBlocks parses a partial HCL string that contains only
|
||||
// top-level provisioner blocks and returns the parsed ProvisionerBlock list.
|
||||
// ParseProvisionerBlocks parses a string containing one or more top-level provisioner blocks
|
||||
// in either HCL or JSON syntax, and returns a slice of parsed ProvisionerBlock objects along
|
||||
// with any diagnostics encountered during parsing.
|
||||
func ParseProvisionerBlocks(blockContent string) ([]*ProvisionerBlock, hcl.Diagnostics) {
|
||||
parser := &Parser{Parser: hclparse.NewParser()}
|
||||
log.Printf("[DEBUG] parsing enforced provisioner block content as HCL")
|
||||
|
||||
file, diags := parser.ParseHCL([]byte(blockContent), "enforced_provisioner.pkr.hcl")
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
if !diags.HasErrors() {
|
||||
log.Printf("[DEBUG] parsed enforced provisioner block content as HCL")
|
||||
return parseProvisionerBlocksFromFile(parser, file, diags)
|
||||
}
|
||||
log.Printf("[DEBUG] failed to parse enforced provisioner block content as HCL, trying JSON fallback")
|
||||
|
||||
// Fallback to HCL-JSON for enforced block content authored in JSON syntax.
|
||||
jsonFile, jsonDiags := parser.ParseJSON([]byte(blockContent), "enforced_provisioner.pkr.json")
|
||||
if jsonDiags.HasErrors() {
|
||||
log.Printf("[DEBUG] failed to parse enforced provisioner block content as JSON")
|
||||
return nil, append(diags, jsonDiags...)
|
||||
}
|
||||
|
||||
provisioners, provisionerDiags := parseProvisionerBlocksFromFile(parser, jsonFile, jsonDiags)
|
||||
if !provisionerDiags.HasErrors() && len(provisioners) > 0 {
|
||||
log.Printf("[DEBUG] parsed enforced provisioner block content as JSON")
|
||||
return provisioners, provisionerDiags
|
||||
}
|
||||
|
||||
// Backward compatibility fallback for legacy JSON shape:
|
||||
// {"provisioners":[{"type":"shell", ...}]}
|
||||
legacyJSON, ok, err := normalizeLegacyEnforcedProvisionersJSON(blockContent)
|
||||
if err == nil && ok {
|
||||
legacyFile, legacyDiags := parser.ParseJSON([]byte(legacyJSON), "enforced_provisioner_legacy.pkr.json")
|
||||
if !legacyDiags.HasErrors() {
|
||||
legacyProvisioners, legacyProvisionerDiags := parseProvisionerBlocksFromFile(parser, legacyFile, legacyDiags)
|
||||
if !legacyProvisionerDiags.HasErrors() && len(legacyProvisioners) > 0 {
|
||||
log.Printf("[DEBUG] parsed enforced provisioner block content as legacy JSON")
|
||||
return legacyProvisioners, legacyProvisionerDiags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if provisionerDiags.HasErrors() {
|
||||
return nil, provisionerDiags
|
||||
}
|
||||
log.Printf("[DEBUG] parsed enforced provisioner block content as JSON but found no valid provisioner blocks")
|
||||
return provisioners, provisionerDiags
|
||||
}
|
||||
|
||||
func normalizeLegacyEnforcedProvisionersJSON(blockContent string) (string, bool, error) {
|
||||
type legacyPayload struct {
|
||||
Provisioners []map[string]interface{} `json:"provisioners"`
|
||||
}
|
||||
|
||||
var payload legacyPayload
|
||||
if err := json.Unmarshal([]byte(blockContent), &payload); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
if len(payload.Provisioners) == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
normalized := make([]map[string]interface{}, 0, len(payload.Provisioners))
|
||||
for _, p := range payload.Provisioners {
|
||||
typeName, ok := p["type"].(string)
|
||||
if !ok || typeName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
cfg := make(map[string]interface{})
|
||||
for k, v := range p {
|
||||
if k == "type" {
|
||||
continue
|
||||
}
|
||||
cfg[k] = v
|
||||
}
|
||||
|
||||
normalized = append(normalized, map[string]interface{}{typeName: cfg})
|
||||
}
|
||||
|
||||
if len(normalized) == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
out := map[string]interface{}{
|
||||
"provisioner": normalized,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
return string(b), true, nil
|
||||
}
|
||||
|
||||
func parseProvisionerBlocksFromFile(parser *Parser, file *hcl.File, diags hcl.Diagnostics) ([]*ProvisionerBlock, hcl.Diagnostics) {
|
||||
|
||||
content, moreDiags := file.Body.Content(enforcedProvisionerSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
|
|
|||
|
|
@ -104,6 +104,63 @@ provisioner "shell" {
|
|||
wantTypes: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "json single shell provisioner",
|
||||
blockContent: `{
|
||||
"provisioner": [
|
||||
{
|
||||
"shell": {
|
||||
"inline": ["echo 'Hello from enforced provisioner JSON'"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json multiple provisioners",
|
||||
blockContent: `{
|
||||
"provisioner": [
|
||||
{
|
||||
"shell": {
|
||||
"inline": ["echo 'first'"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"shell": {
|
||||
"name": "security-scan",
|
||||
"inline": ["echo 'second'"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 2,
|
||||
wantTypes: []string{"shell", "shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid json syntax",
|
||||
blockContent: `{"provisioner": [ { "shell": { "inline": ["test"] } ] }`,
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "legacy json provisioners format",
|
||||
blockContent: `{
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "shell",
|
||||
"inline": ["echo legacy json format"]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -210,3 +267,43 @@ provisioner "shell" {
|
|||
t.Error("Skip() should return false for source in only list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocksJSONWithOptions(t *testing.T) {
|
||||
blockContent := `{
|
||||
"provisioner": [
|
||||
{
|
||||
"shell": {
|
||||
"pause_before": "15s",
|
||||
"max_retries": 2,
|
||||
"only": ["docker.ubuntu"],
|
||||
"inline": ["echo 'json test'"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
blocks, diags := ParseProvisionerBlocks(blockContent)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
if blocks[0].PauseBefore.Seconds() != 15 {
|
||||
t.Errorf("Expected PauseBefore=15s, got %v", blocks[0].PauseBefore)
|
||||
}
|
||||
|
||||
if blocks[0].MaxRetries != 2 {
|
||||
t.Errorf("Expected MaxRetries=2, got %d", blocks[0].MaxRetries)
|
||||
}
|
||||
|
||||
if blocks[0].OnlyExcept.Skip("docker.ubuntu") {
|
||||
t.Error("Skip() should return false for source in only list")
|
||||
}
|
||||
|
||||
if !blocks[0].OnlyExcept.Skip("null.test") {
|
||||
t.Error("Skip() should return true for source not in only list")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import (
|
|||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/hashicorp/hcl/v2/hclwrite"
|
||||
"github.com/hashicorp/hcl/v2/hclparse"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
type HCPPackerRegistryBlock struct {
|
||||
|
|
@ -100,42 +101,108 @@ func (p *Parser) decodeHCPRegistry(block *hcl.Block, cfg *PackerConfig) (*HCPPac
|
|||
return par, diags
|
||||
}
|
||||
|
||||
// ExtractBuildProvisionerHCL extracts all provisioner blocks from the build
|
||||
// blocks in the configuration and returns them as raw HCL content.
|
||||
// This is used to publish provisioner configurations as enforced blocks
|
||||
// to HCP Packer, so that other builds against the same bucket will
|
||||
// automatically have these provisioners injected.
|
||||
// ExtractBuildProvisionerHCL extracts inline commands from all shell
|
||||
// provisioner blocks across every build block and merges them into a single
|
||||
// provisioner "shell" block. This merged block is what gets published to
|
||||
// HCP Packer as an enforced block so that other builds against the same
|
||||
// bucket automatically run these commands.
|
||||
func (cfg *PackerConfig) ExtractBuildProvisionerHCL() (string, error) {
|
||||
sourceFiles := cfg.parser.Files()
|
||||
|
||||
var buf strings.Builder
|
||||
// Re-parse source files with a fresh parser so the bodies are unconsumed.
|
||||
freshParser := hclparse.NewParser()
|
||||
|
||||
buildSchema := &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{Type: buildLabel},
|
||||
},
|
||||
}
|
||||
provisionerSchema := &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{Type: buildProvisionerLabel, LabelNames: []string{"type"}},
|
||||
},
|
||||
}
|
||||
inlineSchema := &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{Name: "inline"},
|
||||
},
|
||||
}
|
||||
|
||||
var allCommands []string
|
||||
|
||||
for filename, file := range sourceFiles {
|
||||
// hclwrite only supports HCL native syntax, skip JSON and variable files
|
||||
if !strings.HasSuffix(filename, hcl2FileExt) {
|
||||
continue
|
||||
}
|
||||
|
||||
wf, diags := hclwrite.ParseConfig(file.Bytes, filename, hcl.Pos{Line: 1, Column: 1})
|
||||
f, diags := freshParser.ParseHCL(file.Bytes, filename)
|
||||
if diags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, block := range wf.Body().Blocks() {
|
||||
if block.Type() != buildLabel {
|
||||
content, _, diags := f.Body.PartialContent(buildSchema)
|
||||
if diags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, buildBlock := range content.Blocks {
|
||||
innerContent, _, diags := buildBlock.Body.PartialContent(provisionerSchema)
|
||||
if diags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, inner := range block.Body().Blocks() {
|
||||
if inner.Type() != buildProvisionerLabel {
|
||||
for _, provBlock := range innerContent.Blocks {
|
||||
if provBlock.Labels[0] != "shell" {
|
||||
continue
|
||||
}
|
||||
|
||||
buf.Write(inner.BuildTokens(nil).Bytes())
|
||||
buf.WriteString("\n")
|
||||
attrContent, _, diags := provBlock.Body.PartialContent(inlineSchema)
|
||||
if diags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
inlineAttr, ok := attrContent.Attributes["inline"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
val, diags := inlineAttr.Expr.Value(nil)
|
||||
if diags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
if !val.CanIterateElements() {
|
||||
continue
|
||||
}
|
||||
|
||||
it := val.ElementIterator()
|
||||
for it.Next() {
|
||||
_, v := it.Element()
|
||||
if v.Type() == cty.String {
|
||||
allCommands = append(allCommands, v.AsString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSpace(buf.String()), nil
|
||||
if len(allCommands) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Build a single merged provisioner "shell" block with all commands.
|
||||
var buf strings.Builder
|
||||
buf.WriteString(`provisioner "shell" {` + "\n")
|
||||
buf.WriteString(" inline = [\n")
|
||||
for i, cmd := range allCommands {
|
||||
buf.WriteString(fmt.Sprintf(" %q", cmd))
|
||||
if i < len(allCommands)-1 {
|
||||
buf.WriteString(",")
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
buf.WriteString(" ]\n")
|
||||
buf.WriteString("}\n")
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ func (p *HCL2Provisioner) HCL2Prepare(buildVars map[string]interface{}) error {
|
|||
ectx = p.evalContext.NewChild()
|
||||
buildValues := map[string]cty.Value{}
|
||||
if !p.evalContext.Variables[buildAccessor].IsNull() {
|
||||
buildValues = p.evalContext.Variables[buildAccessor].AsValueMap()
|
||||
for k, v := range p.evalContext.Variables[buildAccessor].AsValueMap() {
|
||||
buildValues[k] = v
|
||||
}
|
||||
}
|
||||
for k, v := range buildVars {
|
||||
val, err := ConvertPluginConfigValueToHCLValue(v)
|
||||
|
|
|
|||
|
|
@ -43,21 +43,6 @@ func (h *HCLRegistry) PopulateVersion(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Extract provisioner blocks from the build and publish them as enforced
|
||||
// blocks to HCP Packer, so other builds against the same bucket will
|
||||
// automatically have these provisioners injected.
|
||||
blockContent, err := h.configuration.ExtractBuildProvisionerHCL()
|
||||
if err != nil {
|
||||
log.Printf("[WARN] failed to extract provisioner blocks for enforced publishing: %v", err)
|
||||
} else if blockContent != "" {
|
||||
blockName := h.bucket.Name + "-provisioners"
|
||||
if pubErr := h.bucket.PublishEnforcedBlocks(
|
||||
ctx, blockName, blockContent, hcpPackerModels.HashicorpCloudPacker20230101TemplateTypeHCL2,
|
||||
); pubErr != nil {
|
||||
log.Printf("[WARN] failed to publish enforced blocks for bucket %q: %v", h.bucket.Name, pubErr)
|
||||
}
|
||||
}
|
||||
|
||||
err = h.bucket.populateVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -137,7 +122,7 @@ func (h *HCLRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl
|
|||
}
|
||||
|
||||
if len(provBlocks) > 0 {
|
||||
h.ui.Say(fmt.Sprintf("Loaded %d enforced provisioner(s) from HCP block %q", len(provBlocks), eb.Name))
|
||||
h.ui.Say(fmt.Sprintf("Loaded %d enforced provisioner(s) from HCP block %q and template type %q", len(provBlocks), eb.Name, eb.TemplateType))
|
||||
}
|
||||
|
||||
// Inject into each build
|
||||
|
|
@ -156,10 +141,10 @@ func (h *HCLRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl
|
|||
continue
|
||||
}
|
||||
|
||||
log.Printf("[INFO] injecting enforced provisioner %q from block %q into build %q",
|
||||
pb.PType, eb.Name, build.Name())
|
||||
|
||||
build.Provisioners = append(build.Provisioners, coreProv)
|
||||
|
||||
log.Printf("[INFO] injected enforced provisioner %q from block %q into build %q",
|
||||
pb.PType, eb.Name, build.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type EnforcedBlock struct {
|
|||
BlockContent string // Raw HCL content containing provisioner blocks
|
||||
VersionID string
|
||||
Version string
|
||||
TemplateType string
|
||||
}
|
||||
|
||||
// Bucket represents a single bucket on the HCP Packer registry.
|
||||
|
|
@ -160,6 +161,8 @@ func (bucket *Bucket) FetchEnforcedBlocks(ctx context.Context) error {
|
|||
return errors.New("bucket client not initialized, call Initialize first")
|
||||
}
|
||||
|
||||
log.Printf("[INFO] fetching enforced blocks linked to bucket %q", bucket.Name)
|
||||
|
||||
resp, err := bucket.client.GetEnforcedBlocksForBucket(ctx, bucket.Name)
|
||||
if err != nil {
|
||||
// If the API doesn't support enforced blocks yet or returns not found, continue silently
|
||||
|
|
@ -168,6 +171,7 @@ func (bucket *Bucket) FetchEnforcedBlocks(ctx context.Context) error {
|
|||
}
|
||||
|
||||
if resp == nil {
|
||||
log.Printf("[INFO] no enforced blocks response returned for bucket %q", bucket.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -184,10 +188,20 @@ func (bucket *Bucket) FetchEnforcedBlocks(ctx context.Context) error {
|
|||
VersionID: detail.Version.ID,
|
||||
Version: detail.Version.Version,
|
||||
}
|
||||
|
||||
if detail.Version.TemplateType != nil {
|
||||
block.TemplateType = string(*detail.Version.TemplateType)
|
||||
}
|
||||
bucket.EnforcedBlocks = append(bucket.EnforcedBlocks, block)
|
||||
log.Printf("[INFO] linked enforced block found for bucket %q: name=%q id=%q version=%q",
|
||||
bucket.Name, block.Name, block.ID, block.Version)
|
||||
}
|
||||
|
||||
log.Printf("[INFO] fetched %d enforced block(s) for bucket %q", len(bucket.EnforcedBlocks), bucket.Name)
|
||||
if len(bucket.EnforcedBlocks) == 0 {
|
||||
log.Printf("[INFO] no enforced provisioner blocks linked to bucket %q", bucket.Name)
|
||||
}
|
||||
|
||||
log.Printf("[INFO] fetched %d enforced block(s) linked to bucket %q", len(bucket.EnforcedBlocks), bucket.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -247,15 +261,8 @@ func (bucket *Bucket) PublishEnforcedBlocks(
|
|||
}
|
||||
log.Printf("[INFO] created new version for enforced block %q", blockName)
|
||||
} else {
|
||||
// Create new enforced block
|
||||
log.Printf("[INFO] creating enforced block %q for bucket %q", blockName, bucket.Name)
|
||||
_, err := bucket.client.CreateEnforcedBlock(
|
||||
ctx, blockName, blockContent, version, templateType, "", nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create enforced block %q: %w", blockName, err)
|
||||
}
|
||||
log.Printf("[INFO] created enforced block %q", blockName)
|
||||
// Requirement: do not create enforced blocks from CLI.
|
||||
log.Printf("[INFO] enforced block %q does not exist; skipping creation for bucket %q", blockName, bucket.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Reference in a new issue