packer-cn/config.go
Adrien Delorme 0785c2f6fc
build using HCL2 (#8423)
This follows #8232 which added the code to generate the code required to parse
HCL files for each packer component.

All old config files of packer will keep on working the same. Packer takes one
argument. When a directory is passed, all files in the folder with a name
ending with  “.pkr.hcl” or “.pkr.json” will be parsed using the HCL2 format.
When a file ending with “.pkr.hcl” or “.pkr.json” is passed it will be parsed
using the HCL2 format. For every other case; the old packer style will be used.

## 1. the hcl2template pkg can create a packer.Build from a set of HCL (v2) files

I had to make the packer.coreBuild (which is our one and only packer.Build ) a public struct with public fields

## 2. Components interfaces get a new ConfigSpec Method to read a file from an HCL file.

  This is a breaking change for packer plugins.

a packer component can be a: builder/provisioner/post-processor

each component interface now gets a `ConfigSpec() hcldec.ObjectSpec`
which allows packer to tell what is the layout of the hcl2 config meant
to configure that specific component.

This ObjectSpec is sent through the wire (RPC) and a cty.Value is now
sent through the already existing configuration entrypoints:

 Provisioner.Prepare(raws ...interface{}) error
 Builder.Prepare(raws ...interface{}) ([]string, error)
 PostProcessor.Configure(raws ...interface{}) error

close #1768


Example hcl files:

```hcl
// file amazon-ebs-kms-key/run.pkr.hcl
build {
    sources = [
        "source.amazon-ebs.first",
    ]

    provisioner "shell" {
        inline = [
            "sleep 5"
        ]
    }

    post-processor "shell-local" {
        inline = [
            "sleep 5"
        ]
    }
}

// amazon-ebs-kms-key/source.pkr.hcl

source "amazon-ebs" "first" {

    ami_name = "hcl2-test"
    region = "us-east-1"
    instance_type = "t2.micro"

    kms_key_id = "c729958f-c6ba-44cd-ab39-35ab68ce0a6c"
    encrypt_boot = true
    source_ami_filter {
        filters {
          virtualization-type = "hvm"
          name =  "amzn-ami-hvm-????.??.?.????????-x86_64-gp2"
          root-device-type = "ebs"
        }
        most_recent = true
        owners = ["amazon"]
    }
    launch_block_device_mappings {
        device_name = "/dev/xvda"
        volume_size = 20
        volume_type = "gp2"
        delete_on_termination = "true"
    }
    launch_block_device_mappings {
        device_name = "/dev/xvdf"
        volume_size = 500
        volume_type = "gp2"
        delete_on_termination = true
        encrypted = true
    }

    ami_regions = ["eu-central-1"]
    run_tags {
        Name = "packer-solr-something"
        stack-name = "DevOps Tools"
    }
    
    communicator = "ssh"
    ssh_pty = true
    ssh_username = "ec2-user"
    associate_public_ip_address = true
}
```
2019-12-17 11:25:56 +01:00

307 lines
8.7 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
"github.com/hashicorp/packer/command"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/packer/plugin"
"github.com/kardianos/osext"
)
// PACKERSPACE is used to represent the spaces that separate args for a command
// without being confused with spaces in the path to the command itself.
const PACKERSPACE = "-PACKERSPACE-"
type config struct {
DisableCheckpoint bool `json:"disable_checkpoint"`
DisableCheckpointSignature bool `json:"disable_checkpoint_signature"`
PluginMinPort int
PluginMaxPort int
Builders packer.MapOfBuilder
Provisioners packer.MapOfProvisioner
PostProcessors packer.MapOfPostProcessor `json:"post-processors"`
}
// Decodes configuration in JSON format from the given io.Reader into
// the config object pointed to.
func decodeConfig(r io.Reader, c *config) error {
decoder := json.NewDecoder(r)
return decoder.Decode(c)
}
// Discover discovers plugins.
//
// Search the directory of the executable, then the plugins directory, and
// finally the CWD, in that order. Any conflicts will overwrite previously
// found plugins, in that order.
// Hence, the priority order is the reverse of the search order - i.e., the
// CWD has the highest priority.
func (c *config) Discover() error {
// If we are already inside a plugin process we should not need to
// discover anything.
if os.Getenv(plugin.MagicCookieKey) == plugin.MagicCookieValue {
return nil
}
// First, look in the same directory as the executable.
exePath, err := osext.Executable()
if err != nil {
log.Printf("[ERR] Error loading exe directory: %s", err)
} else {
if err := c.discoverExternalComponents(filepath.Dir(exePath)); err != nil {
return err
}
}
// Next, look in the plugins directory.
dir, err := packer.ConfigDir()
if err != nil {
log.Printf("[ERR] Error loading config directory: %s", err)
} else {
if err := c.discoverExternalComponents(filepath.Join(dir, "plugins")); err != nil {
return err
}
}
// Next, look in the CWD.
if err := c.discoverExternalComponents("."); err != nil {
return err
}
// Finally, try to use an internal plugin. Note that this will not override
// any previously-loaded plugins.
if err := c.discoverInternalComponents(); err != nil {
return err
}
return nil
}
// This is a proper packer.BuilderFunc that can be used to load packer.Builder
// implementations from the defined plugins.
func (c *config) StartBuilder(name string) (packer.Builder, error) {
log.Printf("Loading builder: %s\n", name)
return c.Builders.Start(name)
}
// This is a proper implementation of packer.HookFunc that can be used
// to load packer.Hook implementations from the defined plugins.
func (c *config) StarHook(name string) (packer.Hook, error) {
log.Printf("Loading hook: %s\n", name)
return c.pluginClient(name).Hook()
}
// This is a proper packer.PostProcessorFunc that can be used to load
// packer.PostProcessor implementations from defined plugins.
func (c *config) StartPostProcessor(name string) (packer.PostProcessor, error) {
log.Printf("Loading post-processor: %s", name)
return c.PostProcessors.Start(name)
}
// This is a proper packer.ProvisionerFunc that can be used to load
// packer.Provisioner implementations from defined plugins.
func (c *config) StartProvisioner(name string) (packer.Provisioner, error) {
log.Printf("Loading provisioner: %s\n", name)
return c.Provisioners.Start(name)
}
func (c *config) discoverExternalComponents(path string) error {
var err error
if !filepath.IsAbs(path) {
path, err = filepath.Abs(path)
if err != nil {
return err
}
}
externallyUsed := []string{}
pluginPaths, err := c.discoverSingle(filepath.Join(path, "packer-builder-*"))
if err != nil {
return err
}
for plugin := range pluginPaths {
plugin := plugin
c.Builders[plugin] = func() (packer.Builder, error) {
return c.pluginClient(pluginPaths[plugin]).Builder()
}
externallyUsed = append(externallyUsed, plugin)
}
if len(externallyUsed) > 0 {
sort.Strings(externallyUsed)
log.Printf("using external builders %v", externallyUsed)
externallyUsed = nil
}
pluginPaths, err = c.discoverSingle(filepath.Join(path, "packer-post-processor-*"))
if err != nil {
return err
}
for plugin := range pluginPaths {
plugin := plugin
c.PostProcessors[plugin] = func() (packer.PostProcessor, error) {
return c.pluginClient(pluginPaths[plugin]).PostProcessor()
}
externallyUsed = append(externallyUsed, plugin)
}
if len(externallyUsed) > 0 {
sort.Strings(externallyUsed)
log.Printf("using external post-processors %v", externallyUsed)
externallyUsed = nil
}
pluginPaths, err = c.discoverSingle(filepath.Join(path, "packer-provisioner-*"))
if err != nil {
return err
}
for plugin := range pluginPaths {
plugin := plugin
c.Provisioners[plugin] = func() (packer.Provisioner, error) {
return c.pluginClient(pluginPaths[plugin]).Provisioner()
}
externallyUsed = append(externallyUsed, plugin)
}
if len(externallyUsed) > 0 {
sort.Strings(externallyUsed)
log.Printf("using external provisioners %v", externallyUsed)
externallyUsed = nil
}
return nil
}
func (c *config) discoverSingle(glob string) (map[string]string, error) {
matches, err := filepath.Glob(glob)
if err != nil {
return nil, err
}
res := make(map[string]string)
prefix := filepath.Base(glob)
prefix = prefix[:strings.Index(prefix, "*")]
for _, match := range matches {
file := filepath.Base(match)
// One Windows, ignore any plugins that don't end in .exe.
// We could do a full PATHEXT parse, but this is probably good enough.
if runtime.GOOS == "windows" && strings.ToLower(filepath.Ext(file)) != ".exe" {
log.Printf(
"[DEBUG] Ignoring plugin match %s, no exe extension",
match)
continue
}
// If the filename has a ".", trim up to there
if idx := strings.Index(file, "."); idx >= 0 {
file = file[:idx]
}
// Look for foo-bar-baz. The plugin name is "baz"
plugin := file[len(prefix):]
log.Printf("[DEBUG] Discovered plugin: %s = %s", plugin, match)
res[plugin] = match
}
return res, nil
}
func (c *config) discoverInternalComponents() error {
// Get the packer binary path
packerPath, err := osext.Executable()
if err != nil {
log.Printf("[ERR] Error loading exe directory: %s", err)
return err
}
for builder := range command.Builders {
builder := builder
_, found := (c.Builders)[builder]
if !found {
c.Builders[builder] = func() (packer.Builder, error) {
bin := fmt.Sprintf("%s%splugin%spacker-builder-%s",
packerPath, PACKERSPACE, PACKERSPACE, builder)
return c.pluginClient(bin).Builder()
}
}
}
for provisioner := range command.Provisioners {
provisioner := provisioner
_, found := (c.Provisioners)[provisioner]
if !found {
c.Provisioners[provisioner] = func() (packer.Provisioner, error) {
bin := fmt.Sprintf("%s%splugin%spacker-provisioner-%s",
packerPath, PACKERSPACE, PACKERSPACE, provisioner)
return c.pluginClient(bin).Provisioner()
}
}
}
for postProcessor := range command.PostProcessors {
postProcessor := postProcessor
_, found := (c.PostProcessors)[postProcessor]
if !found {
c.PostProcessors[postProcessor] = func() (packer.PostProcessor, error) {
bin := fmt.Sprintf("%s%splugin%spacker-post-processor-%s",
packerPath, PACKERSPACE, PACKERSPACE, postProcessor)
return c.pluginClient(bin).PostProcessor()
}
}
}
return nil
}
func (c *config) pluginClient(path string) *plugin.Client {
originalPath := path
// First attempt to find the executable by consulting the PATH.
path, err := exec.LookPath(path)
if err != nil {
// If that doesn't work, look for it in the same directory
// as the `packer` executable (us).
log.Printf("Plugin could not be found at %s (%v). Checking same directory as executable.", originalPath, err)
exePath, err := osext.Executable()
if err != nil {
log.Printf("Couldn't get current exe path: %s", err)
} else {
log.Printf("Current exe path: %s", exePath)
path = filepath.Join(filepath.Dir(exePath), filepath.Base(originalPath))
}
}
// Check for special case using `packer plugin PLUGIN`
args := []string{}
if strings.Contains(path, PACKERSPACE) {
parts := strings.Split(path, PACKERSPACE)
path = parts[0]
args = parts[1:]
}
// If everything failed, just use the original path and let the error
// bubble through.
if path == "" {
path = originalPath
}
log.Printf("Creating plugin client for path: %s", path)
var config plugin.ClientConfig
config.Cmd = exec.Command(path, args...)
config.Managed = true
config.MinPort = c.PluginMinPort
config.MaxPort = c.PluginMaxPort
return plugin.NewClient(&config)
}