add hcl2template pkg

Package hcl2template defines code to parse hcl2 template files correctly.

In order to configure a packer builder,provisioner,communicator and post
processor.

Checkout the files in testdata/complete/ to see what a packer config could
look like.
This commit is contained in:
Adrien Delorme 2019-10-14 17:02:53 +02:00
parent 078ba7c8c3
commit 2b0e0d4eab
24 changed files with 1863 additions and 0 deletions

101
hcl2template/config_load.go Normal file
View File

@ -0,0 +1,101 @@
package hcl2template
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)
type Artifacts map[ArtifactRef]*Artifact
type Artifact struct {
Type string
Name string
DeclRange hcl.Range
Config hcl.Body
}
func (a *Artifact) Ref() ArtifactRef {
return ArtifactRef{
Type: a.Type,
Name: a.Name,
}
}
type ArtifactRef struct {
Type string
Name string
}
// NoArtifact is the zero value of ArtifactRef, representing the absense of an
// artifact.
var NoArtifact ArtifactRef
func artifactRefFromAbsTraversal(t hcl.Traversal) (ArtifactRef, hcl.Diagnostics) {
var diags hcl.Diagnostics
if len(t) != 3 {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid artifact reference",
Detail: "An artifact reference must have three parts separated by periods: the keyword \"artifact\", the builder type name, and the artifact name.",
Subject: t.SourceRange().Ptr(),
})
return NoArtifact, diags
}
if t.RootName() != "artifact" {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid artifact reference",
Detail: "The first part of an artifact reference must be the keyword \"artifact\".",
Subject: t[0].SourceRange().Ptr(),
})
return NoArtifact, diags
}
btStep, ok := t[1].(hcl.TraverseAttr)
if !ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid artifact reference",
Detail: "The second part of an artifact reference must be an identifier giving the builder type of the artifact.",
Subject: t[1].SourceRange().Ptr(),
})
return NoArtifact, diags
}
nameStep, ok := t[2].(hcl.TraverseAttr)
if !ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid artifact reference",
Detail: "The third part of an artifact reference must be an identifier giving the name of the artifact.",
Subject: t[2].SourceRange().Ptr(),
})
return NoArtifact, diags
}
return ArtifactRef{
Type: btStep.Name,
Name: nameStep.Name,
}, diags
}
func (r ArtifactRef) String() string {
return fmt.Sprintf("%s.%s", r.Type, r.Name)
}
// decodeBodyWithoutSchema is a generic alternative to hcldec.Decode that
// just extracts whatever attributes are present and rejects any nested blocks,
// for compatibility with legacy builders that can't provide explicit schema.
func decodeBodyWithoutSchema(body hcl.Body, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
attrs, diags := body.JustAttributes()
vals := make(map[string]cty.Value)
for name, attr := range attrs {
val, moreDiags := attr.Expr.Value(ctx)
diags = append(diags, moreDiags...)
vals[name] = val
}
return cty.ObjectVal(vals), diags
}

9
hcl2template/docs.go Normal file
View File

@ -0,0 +1,9 @@
// Package hcl2template defines code to parse hcl2 template files correctly.
//
// In order to configure a packer builder,provisioner,communicator and post
// processor.
//
// Checkout the files in testdata/complete/ to see what a packer config could
// look like.
//
package hcl2template

333
hcl2template/load_test.go Normal file
View File

@ -0,0 +1,333 @@
package hcl2template
import (
"testing"
awscommon "github.com/hashicorp/packer/builder/amazon/common"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/packer/helper/communicator"
amazonebs "github.com/hashicorp/packer/builder/amazon/ebs"
"github.com/hashicorp/packer/builder/virtualbox/iso"
"github.com/hashicorp/packer/provisioner/file"
"github.com/hashicorp/packer/provisioner/shell"
amazon_import "github.com/hashicorp/packer/post-processor/amazon-import"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func getBasicParser() *Parser {
return &Parser{
Parser: hclparse.NewParser(),
ProvisionersSchemas: map[string]Decodable{
"shell": &shell.Config{},
"file": &file.Config{},
},
PostProvisionersSchemas: map[string]Decodable{
"amazon-import": &amazon_import.Config{},
},
CommunicatorSchemas: map[string]Decodable{
"ssh": &communicator.SSH{},
"winrm": &communicator.WinRM{},
},
SourceSchemas: map[string]Decodable{
"amazon-ebs": &amazonebs.Config{},
"virtualbox-iso": &iso.Config{},
},
}
}
func TestParser_ParseFile(t *testing.T) {
defaultParser := getBasicParser()
type fields struct {
Parser *hclparse.Parser
}
type args struct {
filename string
cfg *PackerConfig
}
tests := []struct {
name string
parser *Parser
args args
wantPackerConfig *PackerConfig
wantDiags bool
}{
{
"valid " + sourceLabel + " load",
defaultParser,
args{"testdata/sources/basic.pkr.hcl", new(PackerConfig)},
&PackerConfig{
Sources: map[SourceRef]*Source{
SourceRef{
Type: "virtualbox-iso",
Name: "ubuntu-1204",
}: {
Type: "virtualbox-iso",
Name: "ubuntu-1204",
Cfg: &iso.FlatConfig{
HTTPDir: strPtr("xxx"),
ISOChecksum: strPtr("769474248a3897f4865817446f9a4a53"),
ISOChecksumType: strPtr("md5"),
RawSingleISOUrl: strPtr("http://releases.ubuntu.com/12.04/ubuntu-12.04.5-server-amd64.iso"),
BootCommand: []string{"..."},
ShutdownCommand: strPtr("echo 'vagrant' | sudo -S shutdown -P now"),
RawBootWait: strPtr("10s"),
VBoxManage: [][]string{},
VBoxManagePost: [][]string{},
},
},
SourceRef{
Type: "amazon-ebs",
Name: "ubuntu-1604",
}: {
Type: "amazon-ebs",
Name: "ubuntu-1604",
Cfg: &amazonebs.FlatConfig{
RawRegion: strPtr("eu-west-3"),
AMIEncryptBootVolume: boolPtr(true),
InstanceType: strPtr("t2.micro"),
SourceAmiFilter: &awscommon.FlatAmiFilterOptions{
Filters: map[string]string{
"name": "ubuntu/images/*ubuntu-xenial-{16.04}-amd64-server-*",
"root-device-type": "ebs",
"virtualization-type": "hvm",
},
Owners: []string{"099720109477"},
},
AMIMappings: []awscommon.FlatBlockDevice{},
LaunchMappings: []awscommon.FlatBlockDevice{},
},
},
SourceRef{
Type: "amazon-ebs",
Name: "that-ubuntu-1.0",
}: {
Type: "amazon-ebs",
Name: "that-ubuntu-1.0",
Cfg: &amazonebs.FlatConfig{
RawRegion: strPtr("eu-west-3"),
AMIEncryptBootVolume: boolPtr(true),
InstanceType: strPtr("t2.micro"),
SourceAmiFilter: &awscommon.FlatAmiFilterOptions{
MostRecent: boolPtr(true),
},
AMIMappings: []awscommon.FlatBlockDevice{},
LaunchMappings: []awscommon.FlatBlockDevice{},
},
},
},
},
false,
},
{
"valid " + communicatorLabel + " load",
defaultParser,
args{"testdata/communicator/basic.pkr.hcl", new(PackerConfig)},
&PackerConfig{
Communicators: map[CommunicatorRef]*Communicator{
{Type: "ssh", Name: "vagrant"}: {
Type: "ssh", Name: "vagrant",
Cfg: &communicator.FlatSSH{
SSHUsername: strPtr("vagrant"),
SSHPassword: strPtr("s3cr4t"),
SSHClearAuthorizedKeys: boolPtr(true),
SSHHost: strPtr("sssssh.hashicorp.io"),
SSHHandshakeAttempts: intPtr(32),
SSHPort: intPtr(42),
SSHFileTransferMethod: strPtr("scp"),
SSHPrivateKeyFile: strPtr("file.pem"),
SSHPty: boolPtr(false),
SSHTimeout: strPtr("5m"),
SSHAgentAuth: boolPtr(false),
SSHDisableAgentForwarding: boolPtr(true),
SSHBastionHost: strPtr(""),
SSHBastionPort: intPtr(0),
SSHBastionAgentAuth: boolPtr(true),
SSHBastionUsername: strPtr(""),
SSHBastionPassword: strPtr(""),
SSHBastionPrivateKeyFile: strPtr(""),
SSHProxyHost: strPtr("ninja-potatoes.com"),
SSHProxyPort: intPtr(42),
SSHProxyUsername: strPtr("dark-father"),
SSHProxyPassword: strPtr("pickle-rick"),
SSHKeepAliveInterval: strPtr("10s"),
SSHReadWriteTimeout: strPtr("5m"),
},
},
},
},
false,
},
// {
// "duplicate " + sourceLabel, defaultParser,
// args{"testdata/sources/basic.pkr.hcl", &PackerConfig{
// Sources: map[SourceRef]*Source{
// SourceRef{
// Type: "amazon-ebs",
// Name: "ubuntu-1604",
// }: {
// Type: "amazon-ebs",
// Name: "ubuntu-1604",
// Cfg: &amazonebs.FlatConfig{RawRegion: "eu-west-3", InstanceType: "t2.micro"},
// },
// },
// },
// },
// &PackerConfig{
// Sources: map[SourceRef]*Source{
// SourceRef{
// Type: "virtualbox-iso",
// Name: "ubuntu-1204",
// }: {
// Type: "virtualbox-iso",
// Name: "ubuntu-1204",
// Cfg: &iso.FlatConfig{
// HTTPDir: "xxx",
// ISOChecksum: "769474248a3897f4865817446f9a4a53",
// ISOChecksumType: "md5",
// RawSingleISOUrl: "http://releases.ubuntu.com/12.04/ubuntu-12.04.5-server-amd64.iso",
// BootCommand: []string{"..."},
// ShutdownCommand: "echo 'vagrant' | sudo -S shutdown -P now",
// RawBootWait: "10s",
// },
// },
// SourceRef{
// Type: "amazon-ebs",
// Name: "ubuntu-1604",
// }: {
// Type: "amazon-ebs",
// Name: "ubuntu-1604",
// Cfg: &amazonebs.FlatConfig{RawRegion: "eu-west-3", InstanceType: "t2.micro"},
// },
// SourceRef{
// Type: "amazon-ebs",
// Name: "that-ubuntu-1.0",
// }: {
// Type: "amazon-ebs",
// Name: "that-ubuntu-1.0",
// Cfg: &amazonebs.FlatConfig{RawRegion: "eu-west-3", InstanceType: "t2.micro"},
// },
// },
// },
// true,
// },
// {"valid variables load", defaultParser,
// args{"testdata/variables/basic.pkr.hcl", new(PackerConfig)},
// &PackerConfig{
// Variables: PackerV1Variables{
// "image_name": "foo-image-{{user `my_secret`}}",
// "key": "value",
// "my_secret": "foo",
// },
// },
// false,
// },
// {"valid " + buildLabel + " load", defaultParser,
// args{"testdata/build/basic.pkr.hcl", new(PackerConfig)},
// &PackerConfig{
// Builds: Builds{
// {
// Froms: BuildFromList{
// {
// Src: SourceRef{"amazon-ebs", "ubuntu-1604"},
// },
// {
// Src: SourceRef{"virtualbox-iso", "ubuntu-1204"},
// },
// },
// ProvisionerGroups: ProvisionerGroups{
// &ProvisionerGroup{
// CommunicatorRef: CommunicatorRef{"ssh", "vagrant"},
// Provisioners: []Provisioner{
// {Cfg: &shell.FlatConfig{
// Inline: []string{"echo '{{user `my_secret`}}' :D"},
// }},
// {Cfg: &shell.FlatConfig{
// Scripts: []string{"script-1.sh", "script-2.sh"},
// ValidExitCodes: []int{0, 42},
// }},
// {Cfg: &file.FlatConfig{
// Source: "app.tar.gz",
// Destination: "/tmp/app.tar.gz",
// }},
// },
// },
// },
// PostProvisionerGroups: ProvisionerGroups{
// &ProvisionerGroup{
// Provisioners: []Provisioner{
// {Cfg: &amazon_import.FlatConfig{
// Name: "that-ubuntu-1.0",
// }},
// },
// },
// },
// },
// &Build{
// Froms: BuildFromList{
// {
// Src: SourceRef{"amazon", "that-ubuntu-1"},
// },
// },
// ProvisionerGroups: ProvisionerGroups{
// &ProvisionerGroup{
// Provisioners: []Provisioner{
// {Cfg: &shell.FlatConfig{
// Inline: []string{"echo HOLY GUACAMOLE !"},
// }},
// },
// },
// },
// },
// },
// },
// false,
// },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := tt.parser
f, moreDiags := p.ParseHCLFile(tt.args.filename)
if moreDiags != nil {
t.Fatalf("diags: %s", moreDiags)
}
diags := p.ParseFile(f, tt.args.cfg)
if tt.wantDiags == (diags == nil) {
for _, diag := range diags {
t.Errorf("PackerConfig.Load() unexpected diagnostics. %v", diag)
}
t.Error("")
}
if diff := cmp.Diff(tt.wantPackerConfig, tt.args.cfg,
cmpopts.IgnoreUnexported(cty.Value{}),
cmpopts.IgnoreTypes(HCL2Ref{}),
cmpopts.IgnoreTypes([]hcl.Range{}),
cmpopts.IgnoreTypes(hcl.Range{}),
cmpopts.IgnoreInterfaces(struct{ hcl.Expression }{}),
cmpopts.IgnoreInterfaces(struct{ hcl.Body }{}),
); diff != "" {
t.Errorf("PackerConfig.Load() wrong packer config. %s", diff)
}
if t.Failed() {
t.Fatal()
}
})
}
}
func strPtr(s string) *string { return &s }
func intPtr(i int) *int { return &i }
func boolPtr(b bool) *bool { return &b }

182
hcl2template/parser.go Normal file
View File

@ -0,0 +1,182 @@
package hcl2template
import (
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
)
const (
sourceLabel = "source"
variablesLabel = "variables"
buildLabel = "build"
communicatorLabel = "communicator"
)
var configSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{Type: sourceLabel, LabelNames: []string{"type", "name"}},
{Type: variablesLabel},
{Type: buildLabel},
{Type: communicatorLabel, LabelNames: []string{"type", "name"}},
},
}
type Parser struct {
*hclparse.Parser
ProvisionersSchemas map[string]Decodable
PostProvisionersSchemas map[string]Decodable
CommunicatorSchemas map[string]Decodable
SourceSchemas map[string]Decodable
}
const hcl2FileExt = ".pkr.hcl"
func (p *Parser) Parse(filename string) (*PackerConfig, hcl.Diagnostics) {
var diags hcl.Diagnostics
hclFiles := []string{}
jsonFiles := []string{}
if strings.HasSuffix(filename, hcl2FileExt) {
hclFiles = append(hclFiles, hcl2FileExt)
} else if strings.HasSuffix(filename, ".json") {
jsonFiles = append(jsonFiles, hcl2FileExt)
} else {
fileInfos, err := ioutil.ReadDir(filename)
if err != nil {
diag := &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Cannot read hcl directory",
Detail: err.Error(),
}
diags = append(diags, diag)
}
for _, fileInfo := range fileInfos {
if fileInfo.IsDir() {
continue
}
filename := filepath.Join(filename, fileInfo.Name())
if strings.HasSuffix(filename, hcl2FileExt) {
hclFiles = append(hclFiles, filename)
} else if strings.HasSuffix(filename, ".json") {
jsonFiles = append(jsonFiles, filename)
}
}
}
var files []*hcl.File
for _, filename := range hclFiles {
f, moreDiags := p.ParseHCLFile(filename)
diags = append(diags, moreDiags...)
files = append(files, f)
}
for _, filename := range jsonFiles {
f, moreDiags := p.ParseJSONFile(filename)
diags = append(diags, moreDiags...)
files = append(files, f)
}
if diags.HasErrors() {
return nil, diags
}
cfg := &PackerConfig{}
for _, file := range files {
moreDiags := p.ParseFile(file, cfg)
diags = append(diags, moreDiags...)
}
if diags.HasErrors() {
return cfg, diags
}
return cfg, nil
}
// ParseFile filename content into cfg.
//
// ParseFile may be called multiple times with the same cfg on a different file.
//
// ParseFile returns as complete a config as we can manage, even if there are
// errors, since a partial result can be useful for careful analysis by
// development tools such as text editor extensions.
func (p *Parser) ParseFile(f *hcl.File, cfg *PackerConfig) hcl.Diagnostics {
var diags hcl.Diagnostics
content, moreDiags := f.Body.Content(configSchema)
diags = append(diags, moreDiags...)
for _, block := range content.Blocks {
switch block.Type {
case sourceLabel:
if cfg.Sources == nil {
cfg.Sources = map[SourceRef]*Source{}
}
source, moreDiags := p.decodeSource(block, p.SourceSchemas)
diags = append(diags, moreDiags...)
ref := source.Ref()
if existing := cfg.Sources[ref]; existing != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate " + sourceLabel + " block",
Detail: fmt.Sprintf("This "+sourceLabel+" block has the "+
"same builder type and name as a previous block declared "+
"at %s. Each "+sourceLabel+" must have a unique name per builder type.",
existing.HCL2Ref.DeclRange),
Subject: &source.HCL2Ref.DeclRange,
})
continue
}
cfg.Sources[ref] = source
case variablesLabel:
if cfg.Variables == nil {
cfg.Variables = PackerV1Variables{}
}
moreDiags := cfg.Variables.decodeConfig(block)
diags = append(diags, moreDiags...)
case buildLabel:
build, moreDiags := p.decodeBuildConfig(block)
diags = append(diags, moreDiags...)
cfg.Builds = append(cfg.Builds, build)
case communicatorLabel:
if cfg.Communicators == nil {
cfg.Communicators = map[CommunicatorRef]*Communicator{}
}
communicator, moreDiags := p.decodeCommunicatorConfig(block)
diags = append(diags, moreDiags...)
ref := communicator.Ref()
if existing := cfg.Communicators[ref]; existing != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate " + communicatorLabel + " block",
Detail: fmt.Sprintf("This "+communicatorLabel+" block has the "+
"same type and name as a previous block declared "+
"at %s. Each "+communicatorLabel+" must have a unique name per type.",
existing.HCL2Ref.DeclRange),
Subject: &communicator.HCL2Ref.DeclRange,
})
continue
}
cfg.Communicators[ref] = communicator
default:
panic(fmt.Sprintf("unexpected block type %q", block.Type)) // TODO(azr): err
}
}
return diags
}

213
hcl2template/parser_test.go Normal file
View File

@ -0,0 +1,213 @@
package hcl2template
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
awscommon "github.com/hashicorp/packer/builder/amazon/common"
amazonebs "github.com/hashicorp/packer/builder/amazon/ebs"
"github.com/hashicorp/packer/builder/virtualbox/iso"
"github.com/hashicorp/packer/helper/communicator"
amazon_import "github.com/hashicorp/packer/post-processor/amazon-import"
"github.com/hashicorp/packer/provisioner/file"
"github.com/hashicorp/packer/provisioner/shell"
)
func TestParser_Parse(t *testing.T) {
defaultParser := getBasicParser()
type args struct {
filename string
}
tests := []struct {
name string
parser *Parser
args args
wantCfg *PackerConfig
wantDiags bool
}{
{"complete",
defaultParser,
args{"testdata/complete"},
&PackerConfig{
Sources: map[SourceRef]*Source{
SourceRef{
Type: "virtualbox-iso",
Name: "ubuntu-1204",
}: {
Type: "virtualbox-iso",
Name: "ubuntu-1204",
Cfg: &iso.FlatConfig{
HTTPDir: strPtr("xxx"),
ISOChecksum: strPtr("769474248a3897f4865817446f9a4a53"),
ISOChecksumType: strPtr("md5"),
RawSingleISOUrl: strPtr("http://releases.ubuntu.com/12.04/ubuntu-12.04.5-server-amd64.iso"),
BootCommand: []string{"..."},
ShutdownCommand: strPtr("echo 'vagrant' | sudo -S shutdown -P now"),
RawBootWait: strPtr("10s"),
VBoxManage: [][]string{},
VBoxManagePost: [][]string{},
},
},
SourceRef{
Type: "amazon-ebs",
Name: "ubuntu-1604",
}: {
Type: "amazon-ebs",
Name: "ubuntu-1604",
Cfg: &amazonebs.FlatConfig{
RawRegion: strPtr("eu-west-3"),
AMIEncryptBootVolume: boolPtr(true),
InstanceType: strPtr("t2.micro"),
SourceAmiFilter: &awscommon.FlatAmiFilterOptions{
Filters: map[string]string{
"name": "ubuntu/images/*ubuntu-xenial-{16.04}-amd64-server-*",
"root-device-type": "ebs",
"virtualization-type": "hvm",
},
Owners: []string{"099720109477"},
},
AMIMappings: []awscommon.FlatBlockDevice{},
LaunchMappings: []awscommon.FlatBlockDevice{},
},
},
SourceRef{
Type: "amazon-ebs",
Name: "that-ubuntu-1.0",
}: {
Type: "amazon-ebs",
Name: "that-ubuntu-1.0",
Cfg: &amazonebs.FlatConfig{
RawRegion: strPtr("eu-west-3"),
AMIEncryptBootVolume: boolPtr(true),
InstanceType: strPtr("t2.micro"),
SourceAmiFilter: &awscommon.FlatAmiFilterOptions{
MostRecent: boolPtr(true),
},
AMIMappings: []awscommon.FlatBlockDevice{},
LaunchMappings: []awscommon.FlatBlockDevice{},
},
},
},
Communicators: map[CommunicatorRef]*Communicator{
{Type: "ssh", Name: "vagrant"}: {
Type: "ssh", Name: "vagrant",
Cfg: &communicator.FlatSSH{
SSHUsername: strPtr("vagrant"),
SSHPassword: strPtr("s3cr4t"),
SSHClearAuthorizedKeys: boolPtr(true),
SSHHost: strPtr("sssssh.hashicorp.io"),
SSHHandshakeAttempts: intPtr(32),
SSHPort: intPtr(42),
SSHFileTransferMethod: strPtr("scp"),
SSHPrivateKeyFile: strPtr("file.pem"),
SSHPty: boolPtr(false),
SSHTimeout: strPtr("5m"),
SSHAgentAuth: boolPtr(false),
SSHDisableAgentForwarding: boolPtr(true),
SSHBastionHost: strPtr(""),
SSHBastionPort: intPtr(0),
SSHBastionAgentAuth: boolPtr(true),
SSHBastionUsername: strPtr(""),
SSHBastionPassword: strPtr(""),
SSHBastionPrivateKeyFile: strPtr(""),
SSHProxyHost: strPtr("ninja-potatoes.com"),
SSHProxyPort: intPtr(42),
SSHProxyUsername: strPtr("dark-father"),
SSHProxyPassword: strPtr("pickle-rick"),
SSHKeepAliveInterval: strPtr("10s"),
SSHReadWriteTimeout: strPtr("5m"),
},
},
},
Variables: PackerV1Variables{
"image_name": "foo-image-{{user `my_secret`}}",
"key": "value",
"my_secret": "foo",
},
Builds: Builds{
{
Froms: BuildFromList{
{
Src: SourceRef{"amazon-ebs", "ubuntu-1604"},
},
{
Src: SourceRef{"virtualbox-iso", "ubuntu-1204"},
},
},
ProvisionerGroups: ProvisionerGroups{
&ProvisionerGroup{
CommunicatorRef: CommunicatorRef{"ssh", "vagrant"},
Provisioners: []Provisioner{
{Cfg: &shell.FlatConfig{
Inline: []string{"echo '{{user `my_secret`}}' :D"},
}},
{Cfg: &shell.FlatConfig{
Scripts: []string{"script-1.sh", "script-2.sh"},
ValidExitCodes: []int{0, 42},
}},
{Cfg: &file.FlatConfig{
Source: strPtr("app.tar.gz"),
Destination: strPtr("/tmp/app.tar.gz"),
}},
},
},
},
PostProvisionerGroups: ProvisionerGroups{
&ProvisionerGroup{
Provisioners: []Provisioner{
{Cfg: &amazon_import.FlatConfig{
Name: strPtr("that-ubuntu-1.0"),
}},
},
},
},
},
&Build{
Froms: BuildFromList{
{
Src: SourceRef{"amazon", "that-ubuntu-1"},
},
},
ProvisionerGroups: ProvisionerGroups{
&ProvisionerGroup{
Provisioners: []Provisioner{
{Cfg: &shell.FlatConfig{
Inline: []string{"echo HOLY GUACAMOLE !"},
}},
},
},
},
},
},
}, false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotCfg, gotDiags := tt.parser.Parse(tt.args.filename)
if tt.wantDiags == (gotDiags == nil) {
t.Errorf("Parser.Parse() unexpected diagnostics. %s", gotDiags)
}
if diff := cmp.Diff(tt.wantCfg, gotCfg,
cmpopts.IgnoreUnexported(cty.Value{}),
cmpopts.IgnoreTypes(HCL2Ref{}),
cmpopts.IgnoreTypes([]hcl.Range{}),
cmpopts.IgnoreTypes(hcl.Range{}),
cmpopts.IgnoreInterfaces(struct{ hcl.Expression }{}),
cmpopts.IgnoreInterfaces(struct{ hcl.Body }{}),
); diff != "" {
t.Errorf("Parser.Parse() wrong packer config. %s", diff)
}
})
}
}

View File

@ -0,0 +1,66 @@
// starts resources to provision them.
build {
from "src.amazon-ebs.ubuntu-1604" {
ami_name = "that-ubuntu-1.0"
}
from "src.virtualbox-iso.ubuntu-1204" {
// build name is defaulted from the label "src.virtualbox-iso.ubuntu-1204"
outout_dir = "path/"
}
provision {
communicator = "comm.ssh.vagrant"
shell {
inline = [
"echo '{{user `my_secret`}}' :D"
]
}
shell {
valid_exit_codes = [
0,
42,
]
scripts = [
"script-1.sh",
"script-2.sh",
]
// override "vmware-iso" { // TODO(azr): handle common fields
// execute_command = "echo 'password' | sudo -S bash {{.Path}}"
// }
}
file {
source = "app.tar.gz"
destination = "/tmp/app.tar.gz"
// timeout = "5s" // TODO(azr): handle common fields
}
}
post_provision {
amazon-import {
// only = ["src.virtualbox-iso.ubuntu-1204"] // TODO(azr): handle common fields
ami_name = "that-ubuntu-1.0"
}
}
}
build {
// build an ami using the ami from the previous build block.
from "src.amazon.that-ubuntu-1.0" {
ami_name = "fooooobaaaar"
}
provision {
shell {
inline = [
"echo HOLY GUACAMOLE !"
]
}
}
}

View File

@ -0,0 +1,27 @@
communicator "ssh" "vagrant" {
ssh_password = "s3cr4t"
ssh_username = "vagrant"
ssh_agent_auth = false
ssh_bastion_agent_auth = true
ssh_bastion_host = ""
ssh_bastion_password = ""
ssh_bastion_port = 0
ssh_bastion_private_key_file = ""
ssh_bastion_username = ""
ssh_clear_authorized_keys = true
ssh_disable_agent_forwarding = true
ssh_file_transfer_method = "scp"
ssh_handshake_attempts = 32
ssh_host = "sssssh.hashicorp.io"
ssh_port = 42
ssh_keep_alive_interval = "10s"
ssh_private_key_file = "file.pem"
ssh_proxy_host = "ninja-potatoes.com"
ssh_proxy_password = "pickle-rick"
ssh_proxy_port = "42"
ssh_proxy_username = "dark-father"
ssh_pty = false
ssh_read_write_timeout = "5m"
ssh_timeout = "5m"
}

View File

@ -0,0 +1 @@
../build/basic.pkr.hcl

View File

@ -0,0 +1 @@
../communicator/basic.pkr.hcl

View File

@ -0,0 +1 @@
../sources/basic.pkr.hcl

View File

@ -0,0 +1,61 @@
// starts resources to provision them.
build {
from "src.amazon-ebs.ubuntu-1604" {
ami_name = "that-ubuntu-1.0"
}
from "src.virtualbox-iso.ubuntu-1204" {
// build name is defaulted from the label "src.virtualbox-iso.ubuntu-1204"
outout_dir = "path/"
}
provision {
communicator = comm.ssh.vagrant
shell {
inline = [
"echo '{{user `my_secret`}}' :D"
]
}
shell {
script = [
"script-1.sh",
"script-2.sh",
]
override "vmware-iso" {
execute_command = "echo 'password' | sudo -S bash {{.Path}}"
}
}
upload "log.go" "/tmp" {
timeout = "5s"
}
}
post_provision {
amazon-import {
only = ["src.virtualbox-iso.ubuntu-1204"]
ami_name = "that-ubuntu-1.0"
}
}
}
build {
// build an ami using the ami from the previous build block.
from "src.amazon.that-ubuntu-1.0" {
ami_name = "fooooobaaaar"
}
provision {
communicator = comm.ssh.vagrant
shell {
inline = [
"echo HOLY GUACAMOLE !"
]
}
}
}

View File

@ -0,0 +1 @@
../variables/basic.pkr.hcl

View File

@ -0,0 +1,37 @@
// a source represents a reusable setting for a system boot/start.
source "virtualbox-iso" "ubuntu-1204" {
iso_url = "http://releases.ubuntu.com/12.04/ubuntu-12.04.5-server-amd64.iso"
iso_checksum = "769474248a3897f4865817446f9a4a53"
iso_checksum_type = "md5"
boot_wait = "10s"
http_directory = "xxx"
boot_command = ["..."]
shutdown_command = "echo 'vagrant' | sudo -S shutdown -P now"
}
source "amazon-ebs" "ubuntu-1604" {
instance_type = "t2.micro"
encrypt_boot = true
region = "eu-west-3"
source_ami_filter {
filters {
virtualization-type = "hvm"
name = "ubuntu/images/*ubuntu-xenial-{16.04}-amd64-server-*"
root-device-type = "ebs"
}
owners = [
"099720109477"
]
}
}
source "amazon-ebs" "that-ubuntu-1.0" {
instance_type = "t2.micro"
encrypt_boot = true
region = "eu-west-3"
source_ami_filter {
most_recent = true
}
}

View File

@ -0,0 +1,6 @@
variables {
key = "value"
my_secret = "foo"
image_name = "foo-image-{{user `my_secret`}}"
}

View File

@ -0,0 +1,68 @@
package hcl2template
import (
"strings"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
type BuildFromList []BuildFrom
type BuildFrom struct {
// source to take config from
Src SourceRef `hcl:"-"`
HCL2Ref HCL2Ref
}
func sourceRefFromString(in string) SourceRef {
args := strings.Split(in, ".")
if len(args) < 2 {
return NoSource
}
if len(args) > 2 {
// src.type.name
args = args[1:]
}
return SourceRef{
Type: args[0],
Name: args[1],
}
}
func (bf *BuildFrom) decodeConfig(block *hcl.Block) hcl.Diagnostics {
bf.Src = sourceRefFromString(block.Labels[0])
bf.HCL2Ref.DeclRange = block.DefRange
var b struct {
Config hcl.Body `hcl:",remain"`
}
diags := gohcl.DecodeBody(block.Body, nil, &b)
if bf.Src == NoSource {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid " + sourceLabel + " reference",
Detail: "A " + sourceLabel + " type must start with a letter and " +
"may contain only letters, digits, underscores, and dashes." +
"A valid source reference looks like: `src.type.name`",
Subject: &block.LabelRanges[0],
})
}
if !hclsyntax.ValidIdentifier(bf.Src.Type) ||
!hclsyntax.ValidIdentifier(bf.Src.Name) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid " + sourceLabel + " reference",
Detail: "A " + sourceLabel + " type must start with a letter and " +
"may contain only letters, digits, underscores, and dashes." +
"A valid source reference looks like: `src.type.name`",
Subject: &block.LabelRanges[0],
})
}
return diags
}

View File

@ -0,0 +1,61 @@
package hcl2template
import (
"github.com/hashicorp/hcl/v2"
)
const (
buildFromLabel = "from"
buildProvisionnersLabel = "provision"
buildPostProvisionnersLabel = "post_provision"
)
var buildSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{Type: buildFromLabel, LabelNames: []string{"src"}},
{Type: buildProvisionnersLabel},
{Type: buildPostProvisionnersLabel},
},
}
type Build struct {
// Ordered list of provisioner groups
ProvisionerGroups ProvisionerGroups
// Ordered list of post-provisioner groups
PostProvisionerGroups ProvisionerGroups
// Ordered list of output stanzas
Froms BuildFromList
HCL2Ref HCL2Ref
}
type Builds []*Build
func (p *Parser) decodeBuildConfig(block *hcl.Block) (*Build, hcl.Diagnostics) {
build := &Build{}
content, diags := block.Body.Content(buildSchema)
for _, block := range content.Blocks {
switch block.Type {
case buildFromLabel:
bf := BuildFrom{}
moreDiags := bf.decodeConfig(block)
diags = append(diags, moreDiags...)
build.Froms = append(build.Froms, bf)
case buildProvisionnersLabel:
pg, moreDiags := p.decodeProvisionerGroup(block, p.ProvisionersSchemas)
diags = append(diags, moreDiags...)
build.ProvisionerGroups = append(build.ProvisionerGroups, pg)
case buildPostProvisionnersLabel:
pg, moreDiags := p.decodeProvisionerGroup(block, p.PostProvisionersSchemas)
diags = append(diags, moreDiags...)
build.PostProvisionerGroups = append(build.PostProvisionerGroups, pg)
}
}
return build, diags
}

View File

@ -0,0 +1,70 @@
package hcl2template
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
)
// Provisioner represents a parsed provisioner
type Provisioner struct {
// Cfg is a parsed config
Cfg interface{}
}
type ProvisionerGroup struct {
CommunicatorRef CommunicatorRef
Provisioners []Provisioner
HCL2Ref HCL2Ref
}
// ProvisionerGroups is a slice of provision blocks; which contains
// provisioners
type ProvisionerGroups []*ProvisionerGroup
func (p *Parser) decodeProvisionerGroup(block *hcl.Block, provisionerSpecs map[string]Decodable) (*ProvisionerGroup, hcl.Diagnostics) {
var b struct {
Communicator string `hcl:"communicator,optional"`
Remain hcl.Body `hcl:",remain"`
}
diags := gohcl.DecodeBody(block.Body, nil, &b)
pg := &ProvisionerGroup{}
pg.CommunicatorRef = communicatorRefFromString(b.Communicator)
pg.HCL2Ref.DeclRange = block.DefRange
buildSchema := &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{},
}
for k := range provisionerSpecs {
buildSchema.Blocks = append(buildSchema.Blocks, hcl.BlockHeaderSchema{
Type: k,
})
}
content, moreDiags := b.Remain.Content(buildSchema)
diags = append(diags, moreDiags...)
for _, block := range content.Blocks {
provisioner, found := provisionerSpecs[block.Type]
if !found {
diags = append(diags, &hcl.Diagnostic{
Summary: "Unknown " + buildProvisionnersLabel + " type",
Subject: &block.LabelRanges[0],
})
continue
}
flatProvisinerCfg, moreDiags := decodeDecodable(block, nil, provisioner)
diags = append(diags, moreDiags...)
pg.Provisioners = append(pg.Provisioners, Provisioner{flatProvisinerCfg})
}
return pg, diags
}
func (pgs ProvisionerGroups) FirstCommunicatorRef() CommunicatorRef {
if len(pgs) == 0 {
return NoCommunicator
}
return pgs[0].CommunicatorRef
}

View File

@ -0,0 +1,88 @@
package hcl2template
import (
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
type Communicator struct {
// Type of communicator; ex: ssh
Type string
// Given name
Name string
Cfg interface{}
HCL2Ref HCL2Ref
}
func (communicator *Communicator) Ref() CommunicatorRef {
return CommunicatorRef{
Type: communicator.Type,
Name: communicator.Name,
}
}
func (p *Parser) decodeCommunicatorConfig(block *hcl.Block) (*Communicator, hcl.Diagnostics) {
output := &Communicator{}
output.Type = block.Labels[0]
output.Name = block.Labels[1]
output.HCL2Ref.DeclRange = block.DefRange
diags := hcl.Diagnostics{}
communicator, found := p.CommunicatorSchemas[output.Type]
if !found {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unknown " + communicatorLabel + " type " + output.Type,
Detail: "A " + communicatorLabel + " type must start with a letter and " +
"may contain only letters, digits, underscores, and dashes.",
Subject: &block.DefRange,
})
return output, diags
}
flatCommunicator, moreDiags := decodeDecodable(block, nil, communicator)
diags = append(diags, moreDiags...)
output.Cfg = flatCommunicator
if !hclsyntax.ValidIdentifier(output.Name) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid " + communicatorLabel + " name",
Detail: "A " + communicatorLabel + " type must start with a letter and " +
"may contain only letters, digits, underscores, and dashes.",
Subject: &block.DefRange,
})
}
return output, diags
}
type CommunicatorRef struct {
Type string
Name string
}
// NoCommunicator is the zero value of CommunicatorRef, representing the
// absense of Communicator.
var NoCommunicator CommunicatorRef
func communicatorRefFromString(in string) CommunicatorRef {
args := strings.Split(in, ".")
if len(args) < 2 {
return NoCommunicator
}
if len(args) > 2 {
// comm.type.name
args = args[1:]
}
return CommunicatorRef{
Type: args[0],
Name: args[1],
}
}

View File

@ -0,0 +1,56 @@
package hcl2template
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
)
type Decodable interface {
FlatMapstructure() interface{}
}
type SelfSpecified interface {
HCL2Spec() map[string]hcldec.Spec
}
func decodeDecodable(block *hcl.Block, ctx *hcl.EvalContext, dec Decodable) (interface{}, hcl.Diagnostics) {
var diags hcl.Diagnostics
flatCfg := dec.FlatMapstructure()
var spec hcldec.ObjectSpec
if ss, selfSpecified := flatCfg.(SelfSpecified); selfSpecified {
spec = hcldec.ObjectSpec(ss.HCL2Spec())
} else {
diags = append(diags, &hcl.Diagnostic{
Summary: "Unknown type",
Subject: &block.DefRange,
Detail: fmt.Sprintf("Cannot get spec from a %T", flatCfg),
})
return nil, diags
}
val, moreDiags := hcldec.Decode(block.Body, spec, ctx)
diags = append(diags, moreDiags...)
err := gocty.FromCtyValue(val, flatCfg)
if err != nil {
switch err := err.(type) {
case cty.PathError:
diags = append(diags, &hcl.Diagnostic{
Summary: "gocty.FromCtyValue: " + err.Error(),
Subject: &block.DefRange,
Detail: fmt.Sprintf("%v", err.Path),
})
default:
diags = append(diags, &hcl.Diagnostic{
Summary: "gocty.FromCtyValue: " + err.Error(),
Subject: &block.DefRange,
Detail: fmt.Sprintf("%v", err),
})
}
}
return flatCfg, diags
}

View File

@ -0,0 +1,20 @@
package hcl2template
import (
"github.com/hashicorp/hcl/v2"
)
// reference to the source definition in configuration text file
type HCL2Ref struct {
// reference to the source definition in configuration text file
DeclRange hcl.Range
// remainder of unparsed body
Remain hcl.Body
}
// func (hr *HCL2Ref) Blah() {
// // hr.Remain.
// ctyjson.Marshal(nil, nil)
// hr.DeclRange.
// }

View File

@ -0,0 +1,165 @@
package hcl2template
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/packer/template"
)
// PackerConfig represents a loaded packer config
type PackerConfig struct {
Sources map[SourceRef]*Source
Variables PackerV1Variables
Builds Builds
Communicators map[CommunicatorRef]*Communicator
}
type PackerV1Build struct {
Builders []*template.Builder
Provisioners []*template.Provisioner
PostProcessors []*template.PostProcessor
}
func (pkrCfg *PackerConfig) ToV1Build() PackerV1Build {
var diags hcl.Diagnostics
res := PackerV1Build{}
for _, build := range pkrCfg.Builds {
communicator, _ := pkrCfg.Communicators[build.ProvisionerGroups.FirstCommunicatorRef()]
for _, from := range build.Froms {
source, found := pkrCfg.Sources[from.Src]
if !found {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unknown " + sourceLabel + " reference",
Detail: "",
Subject: &from.HCL2Ref.DeclRange,
})
continue
}
// provisioners := build.ProvisionerGroups.FlatProvisioners()
// postProcessors := build.PostProvisionerGroups.FlatProvisioners()
_ = from
_ = source
_ = communicator
// _ = provisioners
// _ = postProcessors
}
}
return res
}
func (pkrCfg *PackerConfig) ToTemplate() (*template.Template, error) {
var result template.Template
// var errs error
result.Comments = nil
result.Variables = pkrCfg.Variables.Variables()
// TODO(azr): add sensitive variables
builder := pkrCfg.ToV1Build()
_ = builder
// // Gather all the post-processors
// if len(r.PostProcessors) > 0 {
// result.PostProcessors = make([][]*PostProcessor, 0, len(r.PostProcessors))
// }
// for i, v := range r.PostProcessors {
// // Parse the configurations. We need to do this because post-processors
// // can take three different formats.
// configs, err := r.parsePostProcessor(i, v)
// if err != nil {
// errs = multierror.Append(errs, err)
// continue
// }
// // Parse the PostProcessors out of the configs
// pps := make([]*PostProcessor, 0, len(configs))
// for j, c := range configs {
// var pp PostProcessor
// if err := r.decoder(&pp, nil).Decode(c); err != nil {
// errs = multierror.Append(errs, fmt.Errorf(
// "post-processor %d.%d: %s", i+1, j+1, err))
// continue
// }
// // Type is required
// if pp.Type == "" {
// errs = multierror.Append(errs, fmt.Errorf(
// "post-processor %d.%d: type is required", i+1, j+1))
// continue
// }
// // Set the raw configuration and delete any special keys
// pp.Config = c
// // The name defaults to the type if it isn't set
// if pp.Name == "" {
// pp.Name = pp.Type
// }
// delete(pp.Config, "except")
// delete(pp.Config, "only")
// delete(pp.Config, "keep_input_artifact")
// delete(pp.Config, "type")
// delete(pp.Config, "name")
// if len(pp.Config) == 0 {
// pp.Config = nil
// }
// pps = append(pps, &pp)
// }
// result.PostProcessors = append(result.PostProcessors, pps)
// }
// // Gather all the provisioners
// if len(r.Provisioners) > 0 {
// result.Provisioners = make([]*Provisioner, 0, len(r.Provisioners))
// }
// for i, v := range r.Provisioners {
// var p Provisioner
// if err := r.decoder(&p, nil).Decode(v); err != nil {
// errs = multierror.Append(errs, fmt.Errorf(
// "provisioner %d: %s", i+1, err))
// continue
// }
// // Type is required before any richer validation
// if p.Type == "" {
// errs = multierror.Append(errs, fmt.Errorf(
// "provisioner %d: missing 'type'", i+1))
// continue
// }
// // Set the raw configuration and delete any special keys
// p.Config = v.(map[string]interface{})
// delete(p.Config, "except")
// delete(p.Config, "only")
// delete(p.Config, "override")
// delete(p.Config, "pause_before")
// delete(p.Config, "type")
// delete(p.Config, "timeout")
// if len(p.Config) == 0 {
// p.Config = nil
// }
// result.Provisioners = append(result.Provisioners, &p)
// }
// // If we have errors, return those with a nil result
// if errs != nil {
// return nil, errs
// }
return &result, nil
}

View File

@ -0,0 +1,113 @@
package hcl2template
import (
"fmt"
"github.com/hashicorp/hcl/v2"
)
// A source field in an HCL file will load into the Source type.
//
type Source struct {
// Type of source; ex: virtualbox-iso
Type string
// Given name; if any
Name string
Cfg interface{}
HCL2Ref HCL2Ref
}
func (p *Parser) decodeSource(block *hcl.Block, sourceSpecs map[string]Decodable) (*Source, hcl.Diagnostics) {
source := &Source{
Type: block.Labels[0],
Name: block.Labels[1],
}
source.HCL2Ref.DeclRange = block.DefRange
var diags hcl.Diagnostics
sourceSpec, found := sourceSpecs[source.Type]
if !found {
diags = append(diags, &hcl.Diagnostic{
Summary: "Unknown " + sourceLabel + " type",
Subject: &block.LabelRanges[0],
})
return source, diags
}
flatSource, moreDiags := decodeDecodable(block, nil, sourceSpec)
diags = append(diags, moreDiags...)
source.Cfg = flatSource
return source, diags
}
func (source *Source) Ref() SourceRef {
return SourceRef{
Type: source.Type,
Name: source.Name,
}
}
type SourceRef struct {
Type string
Name string
}
// NoSource is the zero value of sourceRef, representing the absense of an
// source.
var NoSource SourceRef
func sourceRefFromAbsTraversal(t hcl.Traversal) (SourceRef, hcl.Diagnostics) {
var diags hcl.Diagnostics
if len(t) != 3 {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid " + sourceLabel + " reference",
Detail: "A " + sourceLabel + " reference must have three parts separated by periods: the keyword \"" + sourceLabel + "\", the builder type name, and the source name.",
Subject: t.SourceRange().Ptr(),
})
return NoSource, diags
}
if t.RootName() != sourceLabel {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid " + sourceLabel + " reference",
Detail: "The first part of an source reference must be the keyword \"" + sourceLabel + "\".",
Subject: t[0].SourceRange().Ptr(),
})
return NoSource, diags
}
btStep, ok := t[1].(hcl.TraverseAttr)
if !ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid " + sourceLabel + " reference",
Detail: "The second part of an " + sourceLabel + " reference must be an identifier giving the builder type of the " + sourceLabel + ".",
Subject: t[1].SourceRange().Ptr(),
})
return NoSource, diags
}
nameStep, ok := t[2].(hcl.TraverseAttr)
if !ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid " + sourceLabel + " reference",
Detail: "The third part of an " + sourceLabel + " reference must be an identifier giving the name of the " + sourceLabel + ".",
Subject: t[2].SourceRange().Ptr(),
})
return NoSource, diags
}
return SourceRef{
Type: btStep.Name,
Name: nameStep.Name,
}, diags
}
func (r SourceRef) String() string {
return fmt.Sprintf("%s.%s", r.Type, r.Name)
}

View File

@ -0,0 +1,27 @@
package hcl2template
import (
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/packer/template"
)
type PackerV1Variables map[string]string
// decodeConfig decodes a "variables" section the way packer 1 used to
func (variables *PackerV1Variables) decodeConfig(block *hcl.Block) hcl.Diagnostics {
return gohcl.DecodeBody(block.Body, nil, variables)
}
func (variables PackerV1Variables) Variables() map[string]*template.Variable {
res := map[string]*template.Variable{}
for k, v := range variables {
res[k] = &template.Variable{
Key: k,
Default: v,
}
}
return res
}

View File

@ -0,0 +1,156 @@
package hcl2template
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
)
func translateBuilder(path string) (string, error) {
type ConfigV1 map[string]json.RawMessage
type ConfigV1V2 struct {
Artifact map[string]map[string]json.RawMessage `json:"artifact"`
}
type Type struct {
Name string `json:"name"`
Type string `json:"type"`
}
type PostProcessor struct {
Type string `json:"type"`
Except []string `json:"except"`
Only []string `json:"only"`
}
b, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
c1 := ConfigV1{}
if err := json.Unmarshal(b, &c1); err != nil {
return "", err
}
c12 := ConfigV1V2{}
if err := json.Unmarshal(b, &c12); err != nil {
return "", err
}
rawBuilder, found := c1["builders"]
if !found {
// no v1 builders
return path, nil
}
var tn []Type
if err := json.Unmarshal([]byte(rawBuilder), &tn); err != nil {
return "", err
}
var rawbuilders []json.RawMessage
if err := json.Unmarshal([]byte(rawBuilder), &rawbuilders); err != nil {
return "", err
}
var typePPs []PostProcessor
var rawPPs []json.RawMessage
if rawPP := c1["post-processors"]; len(rawPP) != 0 {
if err := json.Unmarshal([]byte(rawPP), &typePPs); err != nil {
return "", err
}
if err := json.Unmarshal([]byte(rawPP), &rawPPs); err != nil {
return "", err
}
}
for n, tn := range tn {
builderName := tn.Type
if tn.Name != "" {
builderName = tn.Name
}
if c12.Artifact[tn.Type] == nil {
c12.Artifact[tn.Type] = map[string]json.RawMessage{}
}
name := tn.Name
if name == "" {
name = fmt.Sprintf("autotranslated-builder-%d", len(c12.Artifact[tn.Type]))
}
if _, exists := c12.Artifact[tn.Type][name]; exists {
return "", fmt.Errorf("%s-%s is defined in old and new config", tn.Type, name)
}
rawbuilder := rawbuilders[n]
rawbuilder = removeKey(rawbuilder, "name", "only", "type")
c12.Artifact[tn.Type][name] = rawbuilder
for n, pp := range typePPs {
skip := false
for _, except := range pp.Except {
if except == builderName {
skip = true
break
}
}
for _, only := range pp.Only {
if only != builderName {
skip = true
break
}
}
if skip {
continue
}
if c12.Artifact[pp.Type] == nil {
c12.Artifact[pp.Type] = map[string]json.RawMessage{}
}
name := fmt.Sprintf("autotranslated-post-processor-%d", len(c12.Artifact[pp.Type]))
if _, exists := c12.Artifact[tn.Type][name]; exists {
return "", fmt.Errorf("%s-%s is defined in old and new config", tn.Type, name)
}
rawpp := rawPPs[n]
rawpp = rawpp[:len(rawpp)-1]
rawpp = append(rawpp, json.RawMessage(`,"source":"$artifacts.`+tn.Type+`.`+builderName+`"}`)...)
rawpp = removeKey(rawpp, "name", "only", "type")
c12.Artifact[pp.Type][name] = rawpp
log.Printf("%s", rawpp)
}
}
path = strings.TrimSuffix(path, ".json")
path = strings.TrimSuffix(path, ".pk")
path = path + ".v2.pk.json"
file, err := os.Create(path)
if err != nil {
return "", err
}
defer file.Close()
enc := json.NewEncoder(file)
enc.SetIndent("", " ")
return path, enc.Encode(c12)
}
func removeKey(in json.RawMessage, keys ...string) json.RawMessage {
m := map[string]json.RawMessage{}
if err := json.Unmarshal(in, &m); err != nil {
panic(err)
}
for _, key := range keys {
delete(m, key)
}
b, err := json.Marshal(m)
if err != nil {
panic(err)
}
return b
}