2015-05-23 17:48:07 -04:00
|
|
|
package packer
|
|
|
|
|
|
|
|
import (
|
2020-06-02 14:58:33 -04:00
|
|
|
"encoding/json"
|
2015-05-23 17:48:07 -04:00
|
|
|
"fmt"
|
2020-03-12 16:40:56 -04:00
|
|
|
"log"
|
2020-03-11 12:55:40 -04:00
|
|
|
"regexp"
|
2015-05-23 19:12:32 -04:00
|
|
|
"sort"
|
2020-08-05 11:41:20 -04:00
|
|
|
"strconv"
|
2019-07-08 18:39:46 -04:00
|
|
|
"strings"
|
2019-03-13 17:59:05 -04:00
|
|
|
|
|
|
|
ttmp "text/template"
|
2015-05-23 17:48:07 -04:00
|
|
|
|
2020-06-02 14:58:33 -04:00
|
|
|
"github.com/google/go-cmp/cmp"
|
2019-01-10 09:27:02 -05:00
|
|
|
multierror "github.com/hashicorp/go-multierror"
|
|
|
|
version "github.com/hashicorp/go-version"
|
2020-05-08 10:41:47 -04:00
|
|
|
"github.com/hashicorp/hcl/v2"
|
2020-12-17 16:29:25 -05:00
|
|
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
|
|
|
"github.com/hashicorp/packer-plugin-sdk/template"
|
|
|
|
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
2015-05-23 17:48:07 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// Core is the main executor of Packer. If Packer is being used as a
|
|
|
|
// library, this is the struct you'll want to instantiate to get anything done.
|
|
|
|
type Core struct {
|
2015-05-29 18:41:52 -04:00
|
|
|
Template *template.Template
|
|
|
|
|
2015-05-23 17:48:07 -04:00
|
|
|
components ComponentFinder
|
|
|
|
variables map[string]string
|
2015-05-23 19:12:32 -04:00
|
|
|
builds map[string]*template.Builder
|
2015-06-29 14:49:45 -04:00
|
|
|
version string
|
2018-08-01 14:20:52 -04:00
|
|
|
secrets []string
|
2019-01-10 09:27:02 -05:00
|
|
|
|
|
|
|
except []string
|
|
|
|
only []string
|
2015-05-23 17:48:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// CoreConfig is the structure for initializing a new Core. Once a CoreConfig
|
|
|
|
// is used to initialize a Core, it shouldn't be re-used or modified again.
|
|
|
|
type CoreConfig struct {
|
2018-08-10 17:25:14 -04:00
|
|
|
Components ComponentFinder
|
|
|
|
Template *template.Template
|
|
|
|
Variables map[string]string
|
|
|
|
SensitiveVariables []string
|
|
|
|
Version string
|
2019-01-10 09:27:02 -05:00
|
|
|
|
|
|
|
// These are set by command-line flags
|
|
|
|
Except []string
|
|
|
|
Only []string
|
2015-05-23 17:48:07 -04:00
|
|
|
}
|
|
|
|
|
2015-05-25 20:29:10 -04:00
|
|
|
// The function type used to lookup Builder implementations.
|
2020-12-01 16:42:11 -05:00
|
|
|
type BuilderFunc func(name string) (packersdk.Builder, error)
|
2015-05-25 20:29:10 -04:00
|
|
|
|
|
|
|
// The function type used to lookup Hook implementations.
|
2020-11-19 18:10:00 -05:00
|
|
|
type HookFunc func(name string) (packersdk.Hook, error)
|
2015-05-25 20:29:10 -04:00
|
|
|
|
|
|
|
// The function type used to lookup PostProcessor implementations.
|
2020-12-01 17:48:55 -05:00
|
|
|
type PostProcessorFunc func(name string) (packersdk.PostProcessor, error)
|
2015-05-25 20:29:10 -04:00
|
|
|
|
|
|
|
// The function type used to lookup Provisioner implementations.
|
2020-12-01 17:48:55 -05:00
|
|
|
type ProvisionerFunc func(name string) (packersdk.Provisioner, error)
|
2015-05-25 20:29:10 -04:00
|
|
|
|
2019-12-17 05:25:56 -05:00
|
|
|
type BasicStore interface {
|
|
|
|
Has(name string) bool
|
|
|
|
List() (names []string)
|
|
|
|
}
|
|
|
|
|
|
|
|
type BuilderStore interface {
|
|
|
|
BasicStore
|
2020-12-01 16:42:11 -05:00
|
|
|
Start(name string) (packersdk.Builder, error)
|
2019-12-17 05:25:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
type ProvisionerStore interface {
|
|
|
|
BasicStore
|
2020-12-01 17:48:55 -05:00
|
|
|
Start(name string) (packersdk.Provisioner, error)
|
2019-12-17 05:25:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
type PostProcessorStore interface {
|
|
|
|
BasicStore
|
2020-12-01 17:48:55 -05:00
|
|
|
Start(name string) (packersdk.PostProcessor, error)
|
2019-12-17 05:25:56 -05:00
|
|
|
}
|
|
|
|
|
2021-01-20 04:37:16 -05:00
|
|
|
type DatasourceStore interface {
|
|
|
|
BasicStore
|
|
|
|
Start(name string) (packersdk.Datasource, error)
|
|
|
|
}
|
|
|
|
|
2015-05-25 20:29:10 -04:00
|
|
|
// ComponentFinder is a struct that contains the various function
|
|
|
|
// pointers necessary to look up components of Packer such as builders,
|
|
|
|
// commands, etc.
|
|
|
|
type ComponentFinder struct {
|
2019-12-17 05:25:56 -05:00
|
|
|
Hook HookFunc
|
|
|
|
|
|
|
|
// For HCL2
|
|
|
|
BuilderStore BuilderStore
|
|
|
|
ProvisionerStore ProvisionerStore
|
|
|
|
PostProcessorStore PostProcessorStore
|
2021-01-20 04:37:16 -05:00
|
|
|
DatasourceStore DatasourceStore
|
2015-05-25 20:29:10 -04:00
|
|
|
}
|
|
|
|
|
2015-05-23 17:48:07 -04:00
|
|
|
// NewCore creates a new Core.
|
2020-07-24 04:58:03 -04:00
|
|
|
func NewCore(c *CoreConfig) *Core {
|
|
|
|
core := &Core{
|
2015-05-29 18:41:52 -04:00
|
|
|
Template: c.Template,
|
2015-05-23 17:48:07 -04:00
|
|
|
components: c.Components,
|
|
|
|
variables: c.Variables,
|
2015-06-29 14:49:45 -04:00
|
|
|
version: c.Version,
|
2019-01-10 09:27:02 -05:00
|
|
|
only: c.Only,
|
|
|
|
except: c.Except,
|
2015-05-28 17:40:45 -04:00
|
|
|
}
|
2020-07-24 04:58:03 -04:00
|
|
|
return core
|
|
|
|
}
|
2018-08-10 17:25:14 -04:00
|
|
|
|
2020-07-24 04:58:03 -04:00
|
|
|
func (core *Core) Initialize() error {
|
|
|
|
if err := core.validate(); err != nil {
|
|
|
|
return err
|
2015-05-28 17:42:53 -04:00
|
|
|
}
|
2020-07-24 04:58:03 -04:00
|
|
|
if err := core.init(); err != nil {
|
|
|
|
return err
|
2015-05-28 17:40:45 -04:00
|
|
|
}
|
2020-07-24 04:58:03 -04:00
|
|
|
for _, secret := range core.secrets {
|
2020-11-19 17:03:11 -05:00
|
|
|
packersdk.LogSecretFilter.Set(secret)
|
2018-08-10 17:25:14 -04:00
|
|
|
}
|
2015-05-28 17:40:45 -04:00
|
|
|
|
2018-03-13 23:22:32 -04:00
|
|
|
// Go through and interpolate all the build names. We should be able
|
2015-05-29 17:29:32 -04:00
|
|
|
// to do this at this point with the variables.
|
2020-07-24 04:58:03 -04:00
|
|
|
core.builds = make(map[string]*template.Builder)
|
|
|
|
for _, b := range core.Template.Builders {
|
|
|
|
v, err := interpolate.Render(b.Name, core.Context())
|
2015-05-29 17:29:32 -04:00
|
|
|
if err != nil {
|
2020-07-24 04:58:03 -04:00
|
|
|
return fmt.Errorf(
|
2015-05-29 17:29:32 -04:00
|
|
|
"Error interpolating builder '%s': %s",
|
|
|
|
b.Name, err)
|
|
|
|
}
|
|
|
|
|
2020-07-24 04:58:03 -04:00
|
|
|
core.builds[v] = b
|
2015-05-29 17:29:32 -04:00
|
|
|
}
|
2020-07-24 04:58:03 -04:00
|
|
|
return nil
|
2015-05-23 17:48:07 -04:00
|
|
|
}
|
|
|
|
|
2015-05-23 19:12:32 -04:00
|
|
|
// BuildNames returns the builds that are available in this configured core.
|
2020-04-30 10:36:01 -04:00
|
|
|
func (c *Core) BuildNames(only, except []string) []string {
|
|
|
|
|
|
|
|
sort.Strings(only)
|
|
|
|
sort.Strings(except)
|
2020-06-09 09:23:29 -04:00
|
|
|
c.except = except
|
|
|
|
c.only = only
|
2020-04-30 10:36:01 -04:00
|
|
|
|
2015-05-23 19:12:32 -04:00
|
|
|
r := make([]string, 0, len(c.builds))
|
2016-11-01 17:08:04 -04:00
|
|
|
for n := range c.builds {
|
2020-04-30 10:36:01 -04:00
|
|
|
onlyPos := sort.SearchStrings(only, n)
|
|
|
|
foundInOnly := onlyPos < len(only) && only[onlyPos] == n
|
|
|
|
if len(only) > 0 && !foundInOnly {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if pos := sort.SearchStrings(except, n); pos < len(except) && except[pos] == n {
|
|
|
|
continue
|
|
|
|
}
|
2015-05-23 19:12:32 -04:00
|
|
|
r = append(r, n)
|
|
|
|
}
|
|
|
|
sort.Strings(r)
|
|
|
|
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2019-12-17 05:25:56 -05:00
|
|
|
func (c *Core) generateCoreBuildProvisioner(rawP *template.Provisioner, rawName string) (CoreBuildProvisioner, error) {
|
2019-09-24 12:44:19 -04:00
|
|
|
// Get the provisioner
|
2019-12-17 05:25:56 -05:00
|
|
|
cbp := CoreBuildProvisioner{}
|
|
|
|
provisioner, err := c.components.ProvisionerStore.Start(rawP.Type)
|
2019-09-24 12:44:19 -04:00
|
|
|
if err != nil {
|
|
|
|
return cbp, fmt.Errorf(
|
|
|
|
"error initializing provisioner '%s': %s",
|
|
|
|
rawP.Type, err)
|
|
|
|
}
|
|
|
|
if provisioner == nil {
|
|
|
|
return cbp, fmt.Errorf(
|
|
|
|
"provisioner type not found: %s", rawP.Type)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the configuration
|
|
|
|
config := make([]interface{}, 1, 2)
|
|
|
|
config[0] = rawP.Config
|
|
|
|
if rawP.Override != nil {
|
|
|
|
if override, ok := rawP.Override[rawName]; ok {
|
|
|
|
config = append(config, override)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// If we're pausing, we wrap the provisioner in a special pauser.
|
2019-10-31 10:49:34 -04:00
|
|
|
if rawP.PauseBefore != 0 {
|
2019-09-24 12:44:19 -04:00
|
|
|
provisioner = &PausedProvisioner{
|
2019-10-31 10:49:34 -04:00
|
|
|
PauseBefore: rawP.PauseBefore,
|
2019-09-24 12:44:19 -04:00
|
|
|
Provisioner: provisioner,
|
|
|
|
}
|
2019-10-31 10:49:34 -04:00
|
|
|
} else if rawP.Timeout != 0 {
|
2019-09-24 12:44:19 -04:00
|
|
|
provisioner = &TimeoutProvisioner{
|
2019-10-31 10:49:34 -04:00
|
|
|
Timeout: rawP.Timeout,
|
2019-09-24 12:44:19 -04:00
|
|
|
Provisioner: provisioner,
|
|
|
|
}
|
|
|
|
}
|
2020-08-05 11:41:20 -04:00
|
|
|
maxRetries := 0
|
|
|
|
if rawP.MaxRetries != "" {
|
|
|
|
renderedMaxRetries, err := interpolate.Render(rawP.MaxRetries, c.Context())
|
|
|
|
if err != nil {
|
|
|
|
return cbp, fmt.Errorf("failed to interpolate `max_retries`: %s", err.Error())
|
|
|
|
}
|
|
|
|
maxRetries, err = strconv.Atoi(renderedMaxRetries)
|
|
|
|
if err != nil {
|
|
|
|
return cbp, fmt.Errorf("`max_retries` must be a valid integer: %s", err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if maxRetries != 0 {
|
2020-04-16 05:58:54 -04:00
|
|
|
provisioner = &RetriedProvisioner{
|
2020-08-05 11:41:20 -04:00
|
|
|
MaxRetries: maxRetries,
|
2020-04-16 05:58:54 -04:00
|
|
|
Provisioner: provisioner,
|
|
|
|
}
|
|
|
|
}
|
2019-12-17 05:25:56 -05:00
|
|
|
cbp = CoreBuildProvisioner{
|
|
|
|
PType: rawP.Type,
|
|
|
|
Provisioner: provisioner,
|
2019-09-24 12:44:19 -04:00
|
|
|
config: config,
|
|
|
|
}
|
|
|
|
|
|
|
|
return cbp, nil
|
|
|
|
}
|
|
|
|
|
2020-05-14 19:22:51 -04:00
|
|
|
// This is used for json templates to launch the build plugins.
|
|
|
|
// They will be prepared via b.Prepare() later.
|
2020-12-09 06:39:54 -05:00
|
|
|
func (c *Core) GetBuilds(opts GetBuildsOptions) ([]packersdk.Build, hcl.Diagnostics) {
|
2020-05-08 10:41:47 -04:00
|
|
|
buildNames := c.BuildNames(opts.Only, opts.Except)
|
2020-12-09 06:39:54 -05:00
|
|
|
builds := []packersdk.Build{}
|
2020-05-08 10:41:47 -04:00
|
|
|
diags := hcl.Diagnostics{}
|
|
|
|
for _, n := range buildNames {
|
|
|
|
b, err := c.Build(n)
|
|
|
|
if err != nil {
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: fmt.Sprintf("Failed to initialize build %q", n),
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
continue
|
|
|
|
}
|
2020-05-28 04:43:58 -04:00
|
|
|
|
|
|
|
// Now that build plugin has been launched, call Prepare()
|
|
|
|
log.Printf("Preparing build: %s", b.Name())
|
|
|
|
b.SetDebug(opts.Debug)
|
|
|
|
b.SetForce(opts.Force)
|
|
|
|
b.SetOnError(opts.OnError)
|
|
|
|
|
|
|
|
warnings, err := b.Prepare()
|
|
|
|
if err != nil {
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: fmt.Sprintf("Failed to prepare build: %q", n),
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-05-28 14:34:00 -04:00
|
|
|
// Only append builds to list if the Prepare() is successful.
|
|
|
|
builds = append(builds, b)
|
|
|
|
|
2020-05-28 04:43:58 -04:00
|
|
|
if len(warnings) > 0 {
|
|
|
|
for _, warning := range warnings {
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagWarning,
|
|
|
|
Summary: fmt.Sprintf("Warning when preparing build: %q", n),
|
|
|
|
Detail: warning,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2020-05-08 10:41:47 -04:00
|
|
|
}
|
|
|
|
return builds, diags
|
|
|
|
}
|
|
|
|
|
2015-05-23 18:08:50 -04:00
|
|
|
// Build returns the Build object for the given name.
|
2020-12-09 06:39:54 -05:00
|
|
|
func (c *Core) Build(n string) (packersdk.Build, error) {
|
2015-05-23 18:08:50 -04:00
|
|
|
// Setup the builder
|
2015-05-25 21:15:07 -04:00
|
|
|
configBuilder, ok := c.builds[n]
|
2015-05-23 18:08:50 -04:00
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("no such build found: %s", n)
|
|
|
|
}
|
2020-05-14 19:22:51 -04:00
|
|
|
// BuilderStore = config.Builders, gathered in loadConfig() in main.go
|
|
|
|
// For reference, the builtin BuilderStore is generated in
|
|
|
|
// packer/config.go in the Discover() func.
|
|
|
|
|
|
|
|
// the Start command launches the builder plugin of the given type without
|
|
|
|
// calling Prepare() or passing any build-specific details.
|
2019-12-17 05:25:56 -05:00
|
|
|
builder, err := c.components.BuilderStore.Start(configBuilder.Type)
|
2015-05-23 18:08:50 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"error initializing builder '%s': %s",
|
|
|
|
configBuilder.Type, err)
|
|
|
|
}
|
|
|
|
if builder == nil {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"builder type not found: %s", configBuilder.Type)
|
|
|
|
}
|
|
|
|
|
2015-05-26 12:14:29 -04:00
|
|
|
// rawName is the uninterpolated name that we use for various lookups
|
|
|
|
rawName := configBuilder.Name
|
|
|
|
|
|
|
|
// Setup the provisioners for this build
|
2019-12-17 05:25:56 -05:00
|
|
|
provisioners := make([]CoreBuildProvisioner, 0, len(c.Template.Provisioners))
|
2015-05-29 18:41:52 -04:00
|
|
|
for _, rawP := range c.Template.Provisioners {
|
2015-05-26 12:14:29 -04:00
|
|
|
// If we're skipping this, then ignore it
|
2019-01-10 09:27:02 -05:00
|
|
|
if rawP.OnlyExcept.Skip(rawName) {
|
2015-05-26 12:14:29 -04:00
|
|
|
continue
|
|
|
|
}
|
2019-09-24 12:44:19 -04:00
|
|
|
cbp, err := c.generateCoreBuildProvisioner(rawP, rawName)
|
2015-05-26 12:14:29 -04:00
|
|
|
if err != nil {
|
2019-09-24 12:44:19 -04:00
|
|
|
return nil, err
|
2015-05-26 12:14:29 -04:00
|
|
|
}
|
|
|
|
|
2019-09-24 12:44:19 -04:00
|
|
|
provisioners = append(provisioners, cbp)
|
|
|
|
}
|
2015-05-26 12:14:29 -04:00
|
|
|
|
2019-12-17 05:25:56 -05:00
|
|
|
var cleanupProvisioner CoreBuildProvisioner
|
2019-09-24 12:44:19 -04:00
|
|
|
if c.Template.CleanupProvisioner != nil {
|
|
|
|
// This is a special instantiation of the shell-local provisioner that
|
|
|
|
// is only run on error at end of provisioning step before other step
|
|
|
|
// cleanup occurs.
|
|
|
|
cleanupProvisioner, err = c.generateCoreBuildProvisioner(c.Template.CleanupProvisioner, rawName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2015-05-26 12:14:29 -04:00
|
|
|
}
|
|
|
|
}
|
2015-05-23 18:08:50 -04:00
|
|
|
|
2015-05-26 12:28:59 -04:00
|
|
|
// Setup the post-processors
|
2019-12-17 05:25:56 -05:00
|
|
|
postProcessors := make([][]CoreBuildPostProcessor, 0, len(c.Template.PostProcessors))
|
2015-05-29 18:41:52 -04:00
|
|
|
for _, rawPs := range c.Template.PostProcessors {
|
2019-12-17 05:25:56 -05:00
|
|
|
current := make([]CoreBuildPostProcessor, 0, len(rawPs))
|
2015-05-26 12:28:59 -04:00
|
|
|
for _, rawP := range rawPs {
|
2019-02-01 09:50:06 -05:00
|
|
|
if rawP.Skip(rawName) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// -except skips post-processor & build
|
2019-02-01 09:17:09 -05:00
|
|
|
foundExcept := false
|
2019-02-01 09:50:06 -05:00
|
|
|
for _, except := range c.except {
|
2019-02-20 05:07:18 -05:00
|
|
|
if except != "" && except == rawP.Name {
|
2019-02-01 09:17:09 -05:00
|
|
|
foundExcept = true
|
|
|
|
}
|
2019-01-10 09:27:02 -05:00
|
|
|
}
|
2019-02-01 09:17:09 -05:00
|
|
|
if foundExcept {
|
2020-06-09 11:35:53 -04:00
|
|
|
break
|
2015-05-26 12:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get the post-processor
|
2019-12-17 05:25:56 -05:00
|
|
|
postProcessor, err := c.components.PostProcessorStore.Start(rawP.Type)
|
2015-05-26 12:28:59 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"error initializing post-processor '%s': %s",
|
|
|
|
rawP.Type, err)
|
|
|
|
}
|
|
|
|
if postProcessor == nil {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"post-processor type not found: %s", rawP.Type)
|
|
|
|
}
|
|
|
|
|
2019-12-17 05:25:56 -05:00
|
|
|
current = append(current, CoreBuildPostProcessor{
|
|
|
|
PostProcessor: postProcessor,
|
|
|
|
PType: rawP.Type,
|
2020-01-15 17:07:53 -05:00
|
|
|
PName: rawP.Name,
|
2015-05-26 12:28:59 -04:00
|
|
|
config: rawP.Config,
|
2020-06-25 03:36:48 -04:00
|
|
|
KeepInputArtifact: rawP.KeepInputArtifact,
|
2015-05-26 12:28:59 -04:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we have no post-processors in this chain, just continue.
|
|
|
|
if len(current) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
postProcessors = append(postProcessors, current)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO hooks one day
|
|
|
|
|
2020-05-14 19:22:51 -04:00
|
|
|
// Return a structure that contains the plugins, their types, variables, and
|
|
|
|
// the raw builder config loaded from the json template
|
2019-12-17 05:25:56 -05:00
|
|
|
return &CoreBuild{
|
|
|
|
Type: n,
|
|
|
|
Builder: builder,
|
|
|
|
BuilderConfig: configBuilder.Config,
|
|
|
|
BuilderType: configBuilder.Type,
|
|
|
|
PostProcessors: postProcessors,
|
|
|
|
Provisioners: provisioners,
|
|
|
|
CleanupProvisioner: cleanupProvisioner,
|
|
|
|
TemplatePath: c.Template.Path,
|
|
|
|
Variables: c.variables,
|
2015-05-23 18:08:50 -04:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2015-05-29 18:35:55 -04:00
|
|
|
// Context returns an interpolation context.
|
|
|
|
func (c *Core) Context() *interpolate.Context {
|
|
|
|
return &interpolate.Context{
|
2015-05-29 18:41:52 -04:00
|
|
|
TemplatePath: c.Template.Path,
|
2015-05-29 18:35:55 -04:00
|
|
|
UserVariables: c.variables,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-05 11:23:54 -04:00
|
|
|
var ConsoleHelp = strings.TrimSpace(`
|
|
|
|
Packer console JSON Mode.
|
|
|
|
The Packer console allows you to experiment with Packer interpolations.
|
|
|
|
You may access variables in the Packer config you called the console with.
|
|
|
|
|
|
|
|
Type in the interpolation to test and hit <enter> to see the result.
|
|
|
|
|
|
|
|
"variables" will dump all available variables and their values.
|
|
|
|
|
|
|
|
"{{timestamp}}" will output the timestamp, for example "1559855090".
|
|
|
|
|
|
|
|
To exit the console, type "exit" and hit <enter>, or use Control-C.
|
|
|
|
|
|
|
|
/!\ If you would like to start console in hcl2 mode without a config you can
|
|
|
|
use the --config-type=hcl2 option.
|
|
|
|
`)
|
|
|
|
|
|
|
|
func (c *Core) EvaluateExpression(line string) (string, bool, hcl.Diagnostics) {
|
|
|
|
switch {
|
|
|
|
case line == "":
|
|
|
|
return "", false, nil
|
|
|
|
case line == "exit":
|
|
|
|
return "", true, nil
|
|
|
|
case line == "help":
|
|
|
|
return ConsoleHelp, false, nil
|
|
|
|
case line == "variables":
|
|
|
|
varsstring := "\n"
|
|
|
|
for k, v := range c.Context().UserVariables {
|
|
|
|
varsstring += fmt.Sprintf("%s: %+v,\n", k, v)
|
|
|
|
}
|
|
|
|
|
|
|
|
return varsstring, false, nil
|
|
|
|
default:
|
|
|
|
ctx := c.Context()
|
|
|
|
rendered, err := interpolate.Render(line, ctx)
|
|
|
|
var diags hcl.Diagnostics
|
|
|
|
if err != nil {
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Summary: "Interpolation error",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return rendered, false, diags
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-23 05:58:57 -04:00
|
|
|
func (c *Core) InspectConfig(opts InspectConfigOptions) int {
|
|
|
|
|
|
|
|
// Convenience...
|
|
|
|
ui := opts.Ui
|
|
|
|
tpl := c.Template
|
|
|
|
ui.Say("Packer Inspect: JSON mode")
|
|
|
|
|
|
|
|
// Description
|
|
|
|
if tpl.Description != "" {
|
|
|
|
ui.Say("Description:\n")
|
|
|
|
ui.Say(tpl.Description + "\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Variables
|
|
|
|
if len(tpl.Variables) == 0 {
|
|
|
|
ui.Say("Variables:\n")
|
|
|
|
ui.Say(" <No variables>")
|
|
|
|
} else {
|
|
|
|
requiredHeader := false
|
|
|
|
for k, v := range tpl.Variables {
|
|
|
|
for _, sensitive := range tpl.SensitiveVariables {
|
|
|
|
if ok := strings.Compare(sensitive.Default, v.Default); ok == 0 {
|
|
|
|
v.Default = "<sensitive>"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if v.Required {
|
|
|
|
if !requiredHeader {
|
|
|
|
requiredHeader = true
|
|
|
|
ui.Say("Required variables:\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
ui.Machine("template-variable", k, v.Default, "1")
|
|
|
|
ui.Say(" " + k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if requiredHeader {
|
|
|
|
ui.Say("")
|
|
|
|
}
|
|
|
|
|
|
|
|
ui.Say("Optional variables and their defaults:\n")
|
|
|
|
keys := make([]string, 0, len(tpl.Variables))
|
|
|
|
max := 0
|
|
|
|
for k := range tpl.Variables {
|
|
|
|
keys = append(keys, k)
|
|
|
|
if len(k) > max {
|
|
|
|
max = len(k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Strings(keys)
|
|
|
|
|
|
|
|
for _, k := range keys {
|
|
|
|
v := tpl.Variables[k]
|
|
|
|
if v.Required {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
for _, sensitive := range tpl.SensitiveVariables {
|
|
|
|
if ok := strings.Compare(sensitive.Default, v.Default); ok == 0 {
|
|
|
|
v.Default = "<sensitive>"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
padding := strings.Repeat(" ", max-len(k))
|
|
|
|
output := fmt.Sprintf(" %s%s = %s", k, padding, v.Default)
|
|
|
|
|
|
|
|
ui.Machine("template-variable", k, v.Default, "0")
|
|
|
|
ui.Say(output)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ui.Say("")
|
|
|
|
|
|
|
|
// Builders
|
|
|
|
ui.Say("Builders:\n")
|
|
|
|
if len(tpl.Builders) == 0 {
|
|
|
|
ui.Say(" <No builders>")
|
|
|
|
} else {
|
|
|
|
keys := make([]string, 0, len(tpl.Builders))
|
|
|
|
max := 0
|
|
|
|
for k := range tpl.Builders {
|
|
|
|
keys = append(keys, k)
|
|
|
|
if len(k) > max {
|
|
|
|
max = len(k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Strings(keys)
|
|
|
|
|
|
|
|
for _, k := range keys {
|
|
|
|
v := tpl.Builders[k]
|
|
|
|
padding := strings.Repeat(" ", max-len(k))
|
|
|
|
output := fmt.Sprintf(" %s%s", k, padding)
|
|
|
|
if v.Name != v.Type {
|
|
|
|
output = fmt.Sprintf("%s (%s)", output, v.Type)
|
|
|
|
}
|
|
|
|
|
|
|
|
ui.Machine("template-builder", k, v.Type)
|
|
|
|
ui.Say(output)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ui.Say("")
|
|
|
|
|
|
|
|
// Provisioners
|
|
|
|
ui.Say("Provisioners:\n")
|
|
|
|
if len(tpl.Provisioners) == 0 {
|
|
|
|
ui.Say(" <No provisioners>")
|
|
|
|
} else {
|
|
|
|
for _, v := range tpl.Provisioners {
|
|
|
|
ui.Machine("template-provisioner", v.Type)
|
|
|
|
ui.Say(fmt.Sprintf(" %s", v.Type))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ui.Say("\nNote: If your build names contain user variables or template\n" +
|
|
|
|
"functions such as 'timestamp', these are processed at build time,\n" +
|
|
|
|
"and therefore only show in their raw form here.")
|
|
|
|
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
2020-06-02 14:58:33 -04:00
|
|
|
func (c *Core) FixConfig(opts FixConfigOptions) hcl.Diagnostics {
|
|
|
|
var diags hcl.Diagnostics
|
|
|
|
|
|
|
|
// Remove once we have support for the Inplace FixConfigMode
|
|
|
|
if opts.Mode != Diff {
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: fmt.Sprintf("FixConfig only supports template diff; FixConfigMode %d not supported", opts.Mode),
|
|
|
|
})
|
|
|
|
|
|
|
|
return diags
|
|
|
|
}
|
|
|
|
|
|
|
|
var rawTemplateData map[string]interface{}
|
|
|
|
input := make(map[string]interface{})
|
|
|
|
templateData := make(map[string]interface{})
|
|
|
|
if err := json.Unmarshal(c.Template.RawContents, &rawTemplateData); err != nil {
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: fmt.Sprintf("unable to read the contents of the JSON configuration file: %s", err),
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return diags
|
|
|
|
}
|
|
|
|
// Hold off on Diff for now - need to think about displaying to user.
|
|
|
|
// delete empty top-level keys since the fixers seem to add them
|
|
|
|
// willy-nilly
|
|
|
|
for k := range input {
|
|
|
|
ml, ok := input[k].([]map[string]interface{})
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if len(ml) == 0 {
|
|
|
|
delete(input, k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// marshal/unmarshal to make comparable to templateData
|
|
|
|
var fixedData map[string]interface{}
|
|
|
|
// Guaranteed to be valid json, so we can ignore errors
|
|
|
|
j, _ := json.Marshal(input)
|
|
|
|
if err := json.Unmarshal(j, &fixedData); err != nil {
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: fmt.Sprintf("unable to read the contents of the JSON configuration file: %s", err),
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
|
|
|
|
return diags
|
|
|
|
}
|
|
|
|
|
|
|
|
if diff := cmp.Diff(templateData, fixedData); diff != "" {
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Fixable configuration found.\nPlease run `packer fix` to get your build to run correctly.\nSee debug log for more information.",
|
|
|
|
Detail: diff,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return diags
|
|
|
|
}
|
|
|
|
|
2015-05-28 17:42:53 -04:00
|
|
|
// validate does a full validation of the template.
|
2015-05-23 17:48:07 -04:00
|
|
|
//
|
2015-05-28 17:42:53 -04:00
|
|
|
// This will automatically call template.validate() in addition to doing
|
2015-05-23 17:48:07 -04:00
|
|
|
// richer semantic checks around variables and so on.
|
2015-05-28 17:42:53 -04:00
|
|
|
func (c *Core) validate() error {
|
2015-05-23 17:48:07 -04:00
|
|
|
// First validate the template in general, we can't do anything else
|
|
|
|
// unless the template itself is valid.
|
2015-05-29 18:41:52 -04:00
|
|
|
if err := c.Template.Validate(); err != nil {
|
2015-05-23 17:48:07 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-06-29 14:49:45 -04:00
|
|
|
// Validate the minimum version is satisfied
|
|
|
|
if c.Template.MinVersion != "" {
|
|
|
|
versionActual, err := version.NewVersion(c.version)
|
|
|
|
if err != nil {
|
|
|
|
// This shouldn't happen since we set it via the compiler
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
versionMin, err := version.NewVersion(c.Template.MinVersion)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"min_version is invalid: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if versionActual.LessThan(versionMin) {
|
|
|
|
return fmt.Errorf(
|
2015-07-13 22:32:28 -04:00
|
|
|
"This template requires Packer version %s or higher; using %s",
|
2015-06-29 14:49:45 -04:00
|
|
|
versionMin,
|
|
|
|
versionActual)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-23 17:48:07 -04:00
|
|
|
// Validate variables are set
|
|
|
|
var err error
|
2015-05-29 18:41:52 -04:00
|
|
|
for n, v := range c.Template.Variables {
|
2015-05-23 17:48:07 -04:00
|
|
|
if v.Required {
|
|
|
|
if _, ok := c.variables[n]; !ok {
|
|
|
|
err = multierror.Append(err, fmt.Errorf(
|
|
|
|
"required variable not set: %s", n))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: validate all builders exist
|
|
|
|
// TODO: ^^ provisioner
|
|
|
|
// TODO: ^^ post-processor
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
2015-05-28 17:40:45 -04:00
|
|
|
|
2020-03-11 12:55:40 -04:00
|
|
|
func isDoneInterpolating(v string) (bool, error) {
|
|
|
|
// Check for whether the var contains any more references to `user`, wrapped
|
|
|
|
// in interpolation syntax.
|
2020-03-12 13:00:56 -04:00
|
|
|
filter := `{{\s*user\s*\x60.*\x60\s*}}`
|
2020-03-11 12:55:40 -04:00
|
|
|
matched, err := regexp.MatchString(filter, v)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("Can't tell if interpolation is done: %s", err)
|
2015-05-28 17:40:45 -04:00
|
|
|
}
|
2020-03-11 12:55:40 -04:00
|
|
|
if matched {
|
|
|
|
// not done interpolating; there's still a call to "user" in a template
|
|
|
|
// engine
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
// No more calls to "user" as a template engine, so we're done.
|
|
|
|
return true, nil
|
|
|
|
}
|
2019-03-08 17:49:47 -05:00
|
|
|
|
2020-03-11 12:55:40 -04:00
|
|
|
func (c *Core) renderVarsRecursively() (*interpolate.Context, error) {
|
2015-05-29 18:35:55 -04:00
|
|
|
ctx := c.Context()
|
2015-05-29 17:29:32 -04:00
|
|
|
ctx.EnableEnv = true
|
2019-03-07 12:37:31 -05:00
|
|
|
ctx.UserVariables = make(map[string]string)
|
2019-03-08 17:49:47 -05:00
|
|
|
shouldRetry := true
|
|
|
|
changed := false
|
|
|
|
failedInterpolation := ""
|
|
|
|
|
|
|
|
// Why this giant loop? User variables can be recursively defined. For
|
|
|
|
// example:
|
|
|
|
// "variables": {
|
|
|
|
// "foo": "bar",
|
|
|
|
// "baz": "{{user `foo`}}baz",
|
|
|
|
// "bang": "bang{{user `baz`}}"
|
|
|
|
// },
|
|
|
|
// In this situation, we cannot guarantee that we've added "foo" to
|
|
|
|
// UserVariables before we try to interpolate "baz" the first time. We need
|
|
|
|
// to have the option to loop back over in order to add the properly
|
|
|
|
// interpolated "baz" to the UserVariables map.
|
|
|
|
// Likewise, we'd need to loop up to two times to properly add "bang",
|
|
|
|
// since that depends on "baz" being set, which depends on "foo" being set.
|
|
|
|
|
|
|
|
// We break out of the while loop either if all our variables have been
|
|
|
|
// interpolated or if after 100 loops we still haven't succeeded in
|
|
|
|
// interpolating them. Please don't actually nest your variables in 100
|
|
|
|
// layers of other variables. Please.
|
|
|
|
|
2019-06-07 18:56:20 -04:00
|
|
|
// c.Template.Variables is populated by variables defined within the Template
|
|
|
|
// itself
|
|
|
|
// c.variables is populated by variables read in from the command line and
|
|
|
|
// var-files.
|
|
|
|
// We need to read the keys from both, then loop over all of them to figure
|
|
|
|
// out the appropriate interpolations.
|
|
|
|
|
2020-03-11 12:55:40 -04:00
|
|
|
repeatMap := make(map[string]string)
|
2020-03-12 16:40:56 -04:00
|
|
|
allKeys := make([]string, 0)
|
|
|
|
|
2019-06-07 18:56:20 -04:00
|
|
|
// load in template variables
|
|
|
|
for k, v := range c.Template.Variables {
|
2020-03-11 12:55:40 -04:00
|
|
|
repeatMap[k] = v.Default
|
2020-03-12 16:40:56 -04:00
|
|
|
allKeys = append(allKeys, k)
|
2019-06-07 18:56:20 -04:00
|
|
|
}
|
2015-05-28 17:40:45 -04:00
|
|
|
|
2019-06-07 18:56:20 -04:00
|
|
|
// overwrite template variables with command-line-read variables
|
|
|
|
for k, v := range c.variables {
|
2020-03-11 12:55:40 -04:00
|
|
|
repeatMap[k] = v
|
2020-03-12 16:40:56 -04:00
|
|
|
allKeys = append(allKeys, k)
|
|
|
|
}
|
|
|
|
|
|
|
|
// sort map to force the following loop to be deterministic.
|
|
|
|
sort.Strings(allKeys)
|
|
|
|
type keyValue struct {
|
|
|
|
Key string
|
|
|
|
Value string
|
|
|
|
}
|
|
|
|
sortedMap := make([]keyValue, len(repeatMap))
|
|
|
|
for _, k := range allKeys {
|
|
|
|
sortedMap = append(sortedMap, keyValue{k, repeatMap[k]})
|
2019-06-07 18:56:20 -04:00
|
|
|
}
|
2015-05-28 17:40:45 -04:00
|
|
|
|
2020-01-27 13:10:16 -05:00
|
|
|
// Regex to exclude any build function variable or template variable
|
|
|
|
// from interpolating earlier
|
|
|
|
// E.g.: {{ .HTTPIP }} won't interpolate now
|
|
|
|
renderFilter := "{{(\\s|)\\.(.*?)(\\s|)}}"
|
|
|
|
|
2019-06-07 18:56:20 -04:00
|
|
|
for i := 0; i < 100; i++ {
|
|
|
|
shouldRetry = false
|
2020-03-12 16:40:56 -04:00
|
|
|
changed = false
|
|
|
|
deleteKeys := []string{}
|
2019-06-07 18:56:20 -04:00
|
|
|
// First, loop over the variables in the template
|
2020-03-12 16:40:56 -04:00
|
|
|
for _, kv := range sortedMap {
|
2019-03-08 17:49:47 -05:00
|
|
|
// Interpolate the default
|
2020-03-12 16:40:56 -04:00
|
|
|
renderedV, err := interpolate.RenderRegex(kv.Value, ctx, renderFilter)
|
2019-03-13 17:59:05 -04:00
|
|
|
switch err.(type) {
|
|
|
|
case nil:
|
|
|
|
// We only get here if interpolation has succeeded, so something is
|
|
|
|
// different in this loop than in the last one.
|
|
|
|
changed = true
|
2020-03-12 16:40:56 -04:00
|
|
|
c.variables[kv.Key] = renderedV
|
2019-03-13 17:59:05 -04:00
|
|
|
ctx.UserVariables = c.variables
|
2020-03-11 12:55:40 -04:00
|
|
|
// Remove fully-interpolated variables from the map, and flag
|
|
|
|
// variables that still need interpolating for a repeat.
|
2020-03-12 16:40:56 -04:00
|
|
|
done, err := isDoneInterpolating(kv.Value)
|
2020-03-11 12:55:40 -04:00
|
|
|
if err != nil {
|
|
|
|
return ctx, err
|
|
|
|
}
|
|
|
|
if done {
|
2020-03-12 16:40:56 -04:00
|
|
|
deleteKeys = append(deleteKeys, kv.Key)
|
2020-03-11 12:55:40 -04:00
|
|
|
} else {
|
|
|
|
shouldRetry = true
|
|
|
|
}
|
2019-03-13 17:59:05 -04:00
|
|
|
case ttmp.ExecError:
|
2019-07-08 16:49:14 -04:00
|
|
|
castError := err.(ttmp.ExecError)
|
2019-07-08 18:39:46 -04:00
|
|
|
if strings.Contains(castError.Error(), interpolate.ErrVariableNotSetString) {
|
2019-07-08 16:49:14 -04:00
|
|
|
shouldRetry = true
|
2020-03-12 16:40:56 -04:00
|
|
|
failedInterpolation = fmt.Sprintf(`"%s": "%s"; error: %s`, kv.Key, kv.Value, err)
|
2019-07-08 18:39:46 -04:00
|
|
|
} else {
|
2020-03-11 12:55:40 -04:00
|
|
|
return ctx, err
|
2019-07-08 16:49:14 -04:00
|
|
|
}
|
2019-03-13 17:59:05 -04:00
|
|
|
default:
|
2020-03-11 12:55:40 -04:00
|
|
|
return ctx, fmt.Errorf(
|
2019-03-13 17:59:05 -04:00
|
|
|
// unexpected interpolation error: abort the run
|
|
|
|
"error interpolating default value for '%s': %s",
|
2020-03-12 16:40:56 -04:00
|
|
|
kv.Key, err)
|
2019-03-13 17:59:05 -04:00
|
|
|
}
|
2019-03-08 17:49:47 -05:00
|
|
|
}
|
2019-06-07 18:56:20 -04:00
|
|
|
if !shouldRetry {
|
2019-03-08 17:49:47 -05:00
|
|
|
break
|
2015-05-28 17:40:45 -04:00
|
|
|
}
|
2020-03-12 16:40:56 -04:00
|
|
|
|
|
|
|
// Clear completed vars from sortedMap before next loop. Do this one
|
|
|
|
// key at a time because the indices are gonna change ever time you
|
|
|
|
// delete from the map.
|
|
|
|
for _, k := range deleteKeys {
|
|
|
|
for ind, kv := range sortedMap {
|
|
|
|
if kv.Key == k {
|
|
|
|
log.Printf("Deleting kv.Value: %s", kv.Value)
|
|
|
|
sortedMap = append(sortedMap[:ind], sortedMap[ind+1:]...)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
deleteKeys = []string{}
|
2019-03-08 17:49:47 -05:00
|
|
|
}
|
|
|
|
|
2019-11-04 15:54:52 -05:00
|
|
|
if !changed && shouldRetry {
|
2020-03-11 12:55:40 -04:00
|
|
|
return ctx, fmt.Errorf("Failed to interpolate %s: Please make sure that "+
|
2019-03-08 17:49:47 -05:00
|
|
|
"the variable you're referencing has been defined; Packer treats "+
|
2020-08-16 21:39:08 -04:00
|
|
|
"all variables used to interpolate other user variables as "+
|
2019-03-08 17:49:47 -05:00
|
|
|
"required.", failedInterpolation)
|
2015-05-28 17:40:45 -04:00
|
|
|
}
|
|
|
|
|
2020-03-11 12:55:40 -04:00
|
|
|
return ctx, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Core) init() error {
|
|
|
|
if c.variables == nil {
|
|
|
|
c.variables = make(map[string]string)
|
|
|
|
}
|
|
|
|
// Go through the variables and interpolate the environment and
|
|
|
|
// user variables
|
|
|
|
ctx, err := c.renderVarsRecursively()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-08-10 17:25:14 -04:00
|
|
|
for _, v := range c.Template.SensitiveVariables {
|
2019-06-07 18:56:20 -04:00
|
|
|
secret := ctx.UserVariables[v.Key]
|
|
|
|
c.secrets = append(c.secrets, secret)
|
2018-08-10 17:25:14 -04:00
|
|
|
}
|
|
|
|
|
2015-05-28 17:40:45 -04:00
|
|
|
return nil
|
|
|
|
}
|