diff --git a/pkg/provider/terraform/instance/apply_test.go b/pkg/provider/terraform/instance/apply_test.go index 47f5858bd..5c97a05f0 100644 --- a/pkg/provider/terraform/instance/apply_test.go +++ b/pkg/provider/terraform/instance/apply_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + terraform_types "github.com/docker/infrakit/pkg/provider/terraform/instance/types" "github.com/spf13/afero" "github.com/stretchr/testify/require" @@ -26,7 +27,12 @@ func TestRunTerraformApply(t *testing.T) { dir, err := os.Getwd() require.NoError(t, err) dir = path.Join(dir, "aws-two-tier") - terraform := NewTerraformInstancePlugin(dir, 1*time.Second, false, []string{}, nil) + options := terraform_types.Options{ + Dir: dir, + PollInterval: types.FromDuration(2 * time.Minute), + } + terraform, err := NewTerraformInstancePlugin(options, nil) + require.NoError(t, err) p, _ := terraform.(*plugin) err = p.doTerraformApply() require.NoError(t, err) @@ -36,7 +42,13 @@ func TestContinuePollingStandalone(t *testing.T) { dir, err := ioutil.TempDir("", "infrakit-instance-terraform") require.NoError(t, err) defer os.RemoveAll(dir) - terraform := NewTerraformInstancePlugin(dir, 1*time.Second, true, []string{}, nil) + options := terraform_types.Options{ + Dir: dir, + Standalone: true, + PollInterval: types.FromDuration(2 * time.Minute), + } + terraform, err := NewTerraformInstancePlugin(options, nil) + require.NoError(t, err) p, _ := terraform.(*plugin) shoudApply := p.shouldApply() require.True(t, shoudApply) diff --git a/pkg/provider/terraform/instance/cmd/main.go b/pkg/provider/terraform/instance/cmd/main.go index e758b0f73..8c0760485 100644 --- a/pkg/provider/terraform/instance/cmd/main.go +++ b/pkg/provider/terraform/instance/cmd/main.go @@ -12,12 +12,14 @@ import ( plugin_base "github.com/docker/infrakit/pkg/plugin" group_types "github.com/docker/infrakit/pkg/plugin/group/types" terraform "github.com/docker/infrakit/pkg/provider/terraform/instance" + terraform_types "github.com/docker/infrakit/pkg/provider/terraform/instance/types" instance_plugin "github.com/docker/infrakit/pkg/rpc/instance" "github.com/docker/infrakit/pkg/run" "github.com/docker/infrakit/pkg/spi/group" "github.com/docker/infrakit/pkg/spi/instance" "github.com/docker/infrakit/pkg/template" "github.com/docker/infrakit/pkg/types" + "github.com/docker/machine/libmachine/log" "github.com/spf13/cobra" ) @@ -71,7 +73,7 @@ func main() { for _, resourceString := range *importResources { split := strings.Split(resourceString, ":") if len(split) < 2 || len(split) > 3 { - err := fmt.Errorf("Imported resource value is not valid: %v", resourceString) + err = fmt.Errorf("Imported resource value is not valid: %v", resourceString) logger.Error("main", "error", err) panic(err) } @@ -94,14 +96,23 @@ func main() { } resources = append(resources, &res) } - importOpts := terraform.ImportOptions{ - InstanceSpec: importInstSpec, - Resources: resources, + options := terraform_types.Options{ + Dir: *dir, + PollInterval: types.FromDuration(*pollInterval), + Standalone: *standalone, } cli.SetLogLevel(*logLevel) - run.Plugin(plugin_base.DefaultTransport(*name), instance_plugin.PluginServer( - terraform.NewTerraformInstancePlugin(*dir, *pollInterval, *standalone, []string{}, &importOpts)), + plugin, err := terraform.NewTerraformInstancePlugin(options, + &terraform.ImportOptions{ + InstanceSpec: importInstSpec, + Resources: resources, + }, ) + if err != nil { + log.Error("error initializing pluing", "err", err) + panic(err) + } + run.Plugin(plugin_base.DefaultTransport(*name), instance_plugin.PluginServer(plugin)) } cmd.AddCommand(cli.VersionCommand()) diff --git a/pkg/provider/terraform/instance/plugin.go b/pkg/provider/terraform/instance/plugin.go index 93dcf45da..52fe0a074 100644 --- a/pkg/provider/terraform/instance/plugin.go +++ b/pkg/provider/terraform/instance/plugin.go @@ -19,6 +19,7 @@ import ( "github.com/docker/infrakit/pkg/discovery" "github.com/docker/infrakit/pkg/discovery/local" logutil "github.com/docker/infrakit/pkg/log" + terraform_types "github.com/docker/infrakit/pkg/provider/terraform/instance/types" "github.com/docker/infrakit/pkg/spi/instance" "github.com/docker/infrakit/pkg/template" "github.com/docker/infrakit/pkg/types" @@ -105,11 +106,11 @@ type ImportOptions struct { } // NewTerraformInstancePlugin returns an instance plugin backed by disk files. -func NewTerraformInstancePlugin(dir string, pollInterval time.Duration, standalone bool, envs []string, importOpts *ImportOptions) instance.Plugin { - logger.Info("NewTerraformInstancePlugin", "dir", dir) +func NewTerraformInstancePlugin(options terraform_types.Options, importOpts *ImportOptions) (instance.Plugin, error) { + logger.Info("NewTerraformInstancePlugin", "dir", options.Dir) var pluginLookup func() discovery.Plugins - if !standalone { + if !options.Standalone { if err := local.Setup(); err != nil { panic(err) } @@ -121,10 +122,18 @@ func NewTerraformInstancePlugin(dir string, pollInterval time.Duration, standalo return plugins } } + // // Environment varables to include when invoking terraform + envs, err := options.ParseOptionsEnvs() + if err != nil { + logger.Error("NewTerraformInstancePlugin", + "msg", "error parsing configuration Env Options", + "err", err) + return nil, err + } p := plugin{ - Dir: dir, + Dir: options.Dir, fs: afero.NewOsFs(), - pollInterval: pollInterval, + pollInterval: options.PollInterval.Duration(), pluginLookup: pluginLookup, envs: envs, } @@ -140,7 +149,7 @@ func NewTerraformInstancePlugin(dir string, pollInterval time.Duration, standalo // if the current node is the leader. However, when leadership changes, a Provision is // not guaranteed to be executed so we need to create the goroutine now. p.terraformApply() - return &p + return &p, nil } // processImport imports the resource with the given ID based on the instance Spec; diff --git a/pkg/provider/terraform/instance/plugin_test.go b/pkg/provider/terraform/instance/plugin_test.go index 65d33b804..a4fa770e4 100644 --- a/pkg/provider/terraform/instance/plugin_test.go +++ b/pkg/provider/terraform/instance/plugin_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + terraform_types "github.com/docker/infrakit/pkg/provider/terraform/instance/types" "github.com/docker/infrakit/pkg/spi/group" "github.com/docker/infrakit/pkg/spi/instance" "github.com/docker/infrakit/pkg/types" @@ -57,12 +58,47 @@ func TestProcessImportOptions(t *testing.T) { require.NoError(t, err) } +func TestEnvs(t *testing.T) { + dir, err := ioutil.TempDir("", "infrakit-instance-terraform") + require.NoError(t, err) + options := terraform_types.Options{ + Dir: dir, + PollInterval: types.FromDuration(2 * time.Minute), + Envs: *types.AnyString(`["k1=v1", "keyval"]`), + } + _, err = NewTerraformInstancePlugin(options, nil) + require.Error(t, err) + + options.Envs = *types.AnyString(`["k1=v1", "k2=v2"]`) + tf, err := NewTerraformInstancePlugin(options, nil) + require.NoError(t, err) + p, _ := tf.(*plugin) + require.Equal(t, []string{"k1=v1", "k2=v2"}, p.envs) + + options.Envs = *types.AnyString("") + tf, err = NewTerraformInstancePlugin(options, nil) + require.NoError(t, err) + p, _ = tf.(*plugin) + require.Equal(t, []string{}, p.envs) + + options.Envs = nil + tf, err = NewTerraformInstancePlugin(options, nil) + require.NoError(t, err) + p, _ = tf.(*plugin) + require.Equal(t, []string{}, p.envs) +} + // getPlugin returns the terraform instance plugin to use for testing and the // directory where the .tf.json files should be stored func getPlugin(t *testing.T) (*plugin, string) { dir, err := ioutil.TempDir("", "infrakit-instance-terraform") require.NoError(t, err) - tf := NewTerraformInstancePlugin(dir, 120*time.Second, false, []string{}, nil) + options := terraform_types.Options{ + Dir: dir, + PollInterval: types.FromDuration(2 * time.Minute), + } + tf, err := NewTerraformInstancePlugin(options, nil) + require.NoError(t, err) tf.(*plugin).pretend = true p, is := tf.(*plugin) require.True(t, is) @@ -79,7 +115,12 @@ func getPluginDirNotExists(t *testing.T) (*plugin, string) { _, err = os.Stat(dir) require.Error(t, err) require.True(t, os.IsNotExist(err), fmt.Sprintf("Incorrect error, expected NotExist, got %v", err)) - tf := NewTerraformInstancePlugin(dir, 120*time.Second, false, []string{}, nil) + options := terraform_types.Options{ + Dir: dir, + PollInterval: types.FromDuration(2 * time.Minute), + } + tf, err := NewTerraformInstancePlugin(options, nil) + require.NoError(t, err) tf.(*plugin).pretend = true p, is := tf.(*plugin) require.True(t, is) @@ -95,7 +136,12 @@ func getPluginDirNoPerms(t *testing.T) (*plugin, string) { dir = dir + "/noperm" os.Mkdir(dir, 0200) require.NoError(t, err) - tf := NewTerraformInstancePlugin(dir, 120*time.Second, false, []string{}, nil) + options := terraform_types.Options{ + Dir: dir, + PollInterval: types.FromDuration(2 * time.Minute), + } + tf, err := NewTerraformInstancePlugin(options, nil) + require.NoError(t, err) tf.(*plugin).pretend = true p, is := tf.(*plugin) require.True(t, is) diff --git a/pkg/provider/terraform/instance/types/types.go b/pkg/provider/terraform/instance/types/types.go new file mode 100644 index 000000000..ae912ee14 --- /dev/null +++ b/pkg/provider/terraform/instance/types/types.go @@ -0,0 +1,146 @@ +package types + +import ( + "encoding/json" + "fmt" + "strings" + + logutil "github.com/docker/infrakit/pkg/log" + group_types "github.com/docker/infrakit/pkg/plugin/group/types" + "github.com/docker/infrakit/pkg/run/scope" + "github.com/docker/infrakit/pkg/spi/group" + "github.com/docker/infrakit/pkg/spi/instance" + "github.com/docker/infrakit/pkg/template" + "github.com/docker/infrakit/pkg/types" + "github.com/docker/machine/libmachine/log" +) + +var ( + logger = logutil.New("module", "provider/terraform/instance/types") +) + +// Resource defines a resource to import +type Resource struct { + // Terraform resource type + ResourceType string + + // Resource name in the group spec + ResourceName string + + // ID of the resource to import + ResourceID string + + // IDs of the properties to exclude from the instance spec + ExcludePropIDs []string +} + +// Options capture the options for starting up the plugin. +type Options struct { + // Dir for storing plan files + Dir string + + // PollInterval is the Terraform polling interval + PollInterval types.Duration + + // Standalone - set if running standalone, disables manager leadership verification + Standalone bool + + // ImportGroupSpecURL defines the group spec that the instance is imported into. + ImportGroupSpecURL string + + // ImportResources defines the instances to import + ImportResources []Resource + + // ImportGroupID defines the group ID to import the resource into (optional) + ImportGroupID string + + // NewOption is an example... see the plugins.json file in this directory. + NewOption string + + // Envs are the environment variables to include when invoking terraform + Envs types.Any +} + +// ParseOptionsEnvs processes the data to create a key=value slice of strings +func (o Options) ParseOptionsEnvs() ([]string, error) { + envs := []string{} + if o.Envs == nil || len(o.Envs.Bytes()) == 0 { + return envs, nil + } + err := json.Unmarshal(o.Envs.Bytes(), &envs) + if err != nil { + return envs, fmt.Errorf("Failed to unmarshall Options.Envs data: %v", err) + } + // Must be key=value pairs + for _, val := range envs { + if !strings.Contains(val, "=") { + return []string{}, fmt.Errorf("Env var is missing '=' character: %v", val) + } + } + return envs, err +} + +// ParseInstanceSpecFromGroup parses the instance.Spec from the group.Spec and adds +// in the tags that should be set on the imported instance +func (o Options) ParseInstanceSpecFromGroup(scope scope.Scope) (*instance.Spec, error) { + if o.ImportGroupSpecURL == "" { + log.Info("No group spec URL specified for import") + return nil, nil + } + var groupSpec group.Spec + t, err := scope.TemplateEngine(o.ImportGroupSpecURL, template.Options{MultiPass: false}) + if err != nil { + logger.Error("ParseInstanceSpecFromGroup", + "msg", "Failed to create template", + "spec", o.ImportGroupSpecURL, + "err", err) + return nil, err + } + template, err := t.Render(nil) + if err != nil { + logger.Error("ParseInstanceSpecFromGroup", + "msg", "Failed to render template", + "spec", o.ImportGroupSpecURL, + "err", err) + return nil, err + } + if err = types.AnyString(template).Decode(&groupSpec); err != nil { + logger.Error("ParseInstanceSpecFromGroup", + "msg", "Failed to decode template", + "spec", o.ImportGroupSpecURL, + "err", err) + return nil, err + } + // Get the instance properties we care about + groupProps, err := group_types.ParseProperties(groupSpec) + if err != nil { + return nil, err + } + + // Add in the bootstrap tag and (if set) the group ID + tags := map[string]string{ + group.ConfigSHATag: "bootstrap", + } + // The group ID should match the spec + if o.ImportGroupID != "" { + if string(groupSpec.ID) != o.ImportGroupID { + return nil, + fmt.Errorf("Given spec ID '%v' does not match given group ID '%v'", string(groupSpec.ID), o.ImportGroupID) + } + tags[group.GroupTag] = o.ImportGroupID + } + // Use the first logical ID if set + if len(groupProps.Allocation.LogicalIDs) > 0 { + tags[instance.LogicalIDTag] = string(groupProps.Allocation.LogicalIDs[0]) + } + + spec := instance.Spec{ + Properties: groupProps.Instance.Properties, + Tags: tags, + } + logger.Info("ParseInstanceSpecFromGroup", + "msg", "Successfully processed instance spec from group", + "group", groupSpec.ID, + "spec", spec) + return &spec, nil +} diff --git a/pkg/provider/terraform/instance/types/types_test.go b/pkg/provider/terraform/instance/types/types_test.go new file mode 100644 index 000000000..068832868 --- /dev/null +++ b/pkg/provider/terraform/instance/types/types_test.go @@ -0,0 +1,189 @@ +package types + +import ( + "testing" + + "github.com/docker/infrakit/pkg/discovery" + "github.com/docker/infrakit/pkg/discovery/local" + "github.com/docker/infrakit/pkg/run/scope" + "github.com/docker/infrakit/pkg/spi/group" + "github.com/docker/infrakit/pkg/spi/instance" + "github.com/docker/infrakit/pkg/types" + "github.com/stretchr/testify/require" +) + +func TestParseOptionsEnvsInvalidJSON(t *testing.T) { + o := Options{Envs: *types.AnyString("not-json")} + _, err := o.ParseOptionsEnvs() + require.Error(t, err) +} + +func TestParseOptionsEnvsNil(t *testing.T) { + o := Options{Envs: nil} + envs, err := o.ParseOptionsEnvs() + require.NoError(t, err) + require.Equal(t, []string{}, envs) +} + +func TestParseOptionsEnvsEmpytString(t *testing.T) { + o := Options{Envs: *types.AnyString("")} + envs, err := o.ParseOptionsEnvs() + require.NoError(t, err) + require.Equal(t, []string{}, envs) +} + +func TestParseOptionsEnvs(t *testing.T) { + o := Options{Envs: *types.AnyString(`["k1=v1", "k2=v2"]`)} + envs, err := o.ParseOptionsEnvs() + require.NoError(t, err) + require.Equal(t, []string{"k1=v1", "k2=v2"}, envs) +} + +func TestParseOptionsEnvsNotKeyValuePairs(t *testing.T) { + o := Options{Envs: *types.AnyString(`["k1=v1", "keyval"]`)} + envs, err := o.ParseOptionsEnvs() + require.Error(t, err) + require.Equal(t, + "Env var is missing '=' character: keyval", + err.Error()) + require.Equal(t, []string{}, envs) +} + +func plugins() discovery.Plugins { + d, err := local.NewPluginDiscovery() + if err != nil { + panic(err) + } + return d +} + +func TestParseInstanceSpecFromGroupEmptyGroupUrl(t *testing.T) { + o := Options{ImportGroupID: "managers"} + spec, err := o.ParseInstanceSpecFromGroup(scope.DefaultScope(plugins)) + require.NoError(t, err) + require.Nil(t, spec) +} + +func TestParseInstanceSpecFromGroupInvalidJSON(t *testing.T) { + o := Options{ + ImportGroupID: "managers", + ImportGroupSpecURL: "str://not-json", + } + _, err := o.ParseInstanceSpecFromGroup(scope.DefaultScope(plugins)) + require.Error(t, err) +} + +func TestParseInstanceSpecFromGroupInvalidGroupSpec(t *testing.T) { + o := Options{ + ImportGroupID: "managers", + ImportGroupSpecURL: "str://{{ nosuchfn }}", + } + _, err := o.ParseInstanceSpecFromGroup(scope.DefaultScope(plugins)) + require.Error(t, err) +} + +func TestParseInstanceSpecFromGroup(t *testing.T) { + groupSpecURL := `str:// +{ + "ID": "managers", + "Properties": { + "instance": { + "Properties": {"resource": {"aws_instance": {}}} + } + } +}` + groupID := "managers" + o := Options{ + ImportGroupID: groupID, + ImportGroupSpecURL: groupSpecURL, + } + instSpec, err := o.ParseInstanceSpecFromGroup(scope.DefaultScope(plugins)) + require.NoError(t, err) + require.Equal(t, + instance.Spec{ + Properties: types.AnyString(`{"resource": {"aws_instance": {}}}`), + Tags: map[string]string{ + group.ConfigSHATag: "bootstrap", + group.GroupTag: groupID, + }, + }, + *instSpec) +} + +func TestParseInstanceSpecFromGroupLogicalID(t *testing.T) { + groupSpecURL := `str:// +{ + "ID": "managers", + "Properties": { + "Allocation": { + "LogicalIDs": ["mgr1", "mgr2", "mgr3"] + }, + "instance": { + "Properties": {"resource": {"aws_instance": {}}} + } + } +}` + groupID := "managers" + o := Options{ + ImportGroupID: groupID, + ImportGroupSpecURL: groupSpecURL, + } + instSpec, err := o.ParseInstanceSpecFromGroup(scope.DefaultScope(plugins)) + require.NoError(t, err) + require.Equal(t, + instance.Spec{ + Properties: types.AnyString(`{"resource": {"aws_instance": {}}}`), + Tags: map[string]string{ + group.ConfigSHATag: "bootstrap", + group.GroupTag: groupID, + instance.LogicalIDTag: "mgr1", + }, + }, + *instSpec) +} + +func TestParseInstanceSpecFromGroupNoGroupIDSpecified(t *testing.T) { + groupSpecURL := `str:// +{ + "ID": "managers", + "Properties": { + "instance": { + "Properties": {"resource": {"aws_instance": {}}} + } + } +}` + o := Options{ + ImportGroupID: "", + ImportGroupSpecURL: groupSpecURL, + } + instSpec, err := o.ParseInstanceSpecFromGroup(scope.DefaultScope(plugins)) + require.NoError(t, err) + require.Equal(t, + instance.Spec{ + Properties: types.AnyString(`{"resource": {"aws_instance": {}}}`), + Tags: map[string]string{ + group.ConfigSHATag: "bootstrap", + }, + }, + *instSpec) +} + +func TestParseInstanceSpecFromGroupNonMatchingGroupID(t *testing.T) { + groupSpecURL := `str:// +{ + "ID": "managers", + "Properties": { + "instance": { + "Properties": {"resource": {"aws_instance": {}}} + } + } +}` + o := Options{ + ImportGroupID: "not-managers", + ImportGroupSpecURL: groupSpecURL, + } + _, err := o.ParseInstanceSpecFromGroup(scope.DefaultScope(plugins)) + require.Equal(t, + "Given spec ID 'managers' does not match given group ID 'not-managers'", + err.Error()) +} diff --git a/pkg/run/v0/terraform/terraform.go b/pkg/run/v0/terraform/terraform.go index 616ba2a29..969682bd1 100644 --- a/pkg/run/v0/terraform/terraform.go +++ b/pkg/run/v0/terraform/terraform.go @@ -1,25 +1,20 @@ package terraform import ( - "encoding/json" "fmt" "os" "os/exec" "path/filepath" - "strings" "time" "github.com/docker/infrakit/pkg/launch/inproc" logutil "github.com/docker/infrakit/pkg/log" "github.com/docker/infrakit/pkg/plugin" - group_types "github.com/docker/infrakit/pkg/plugin/group/types" terraform "github.com/docker/infrakit/pkg/provider/terraform/instance" + terraform_types "github.com/docker/infrakit/pkg/provider/terraform/instance/types" "github.com/docker/infrakit/pkg/run" "github.com/docker/infrakit/pkg/run/local" "github.com/docker/infrakit/pkg/run/scope" - "github.com/docker/infrakit/pkg/spi/group" - "github.com/docker/infrakit/pkg/spi/instance" - "github.com/docker/infrakit/pkg/template" "github.com/docker/infrakit/pkg/types" ) @@ -39,51 +34,9 @@ func init() { inproc.Register(Kind, Run, DefaultOptions) } -// ImportResourceOptions defines a resource to import -type ImportResourceOptions struct { - // Terraform resource type - ResourceType string - - // Resource name in the group spec - ResourceName string - - // ID of the resource to import - ResourceID string - - // IDs of the properties to exclude from the instance spec - ExcludePropIDs []string -} - -// Options capture the options for starting up the plugin. -type Options struct { - // Dir for storing plan files - Dir string - - // PollInterval is the Terraform polling interval - PollInterval types.Duration - - // Standalone - set if running standalone, disables manager leadership verification - Standalone bool - - // ImportGroupSpecURL defines the group spec that the instance is imported into. - ImportGroupSpecURL string - - // ImportResources defines the instances to import - ImportResources []ImportResourceOptions - - // ImportGroupID defines the group ID to import the resource into (optional) - ImportGroupID string - - // NewOption is an example... see the plugins.json file in this directory. - NewOption string - - // Envs are the environment variables to include when invoking terraform - Envs types.Any -} - // DefaultOptions return an Options with default values filled in. If you want to expose these to the CLI, // simply get this struct and bind the fields to the flags. -var DefaultOptions = Options{ +var DefaultOptions = terraform_types.Options{ Dir: local.Getenv(EnvDir, filepath.Join(local.InfrakitHome(), "terraform")), PollInterval: types.FromDuration(30 * time.Second), Standalone: false, @@ -107,7 +60,7 @@ func Run(scope scope.Scope, name plugin.Name, return } - importInstSpec, err := parseInstanceSpecFromGroup(scope, options.ImportGroupSpecURL, options.ImportGroupID) + importInstSpec, err := options.ParseInstanceSpecFromGroup(scope) if err != nil { // If we cannot parse the group spec then we cannot import the resource, the plugin should // not start since terraform is not managing the resource @@ -133,20 +86,19 @@ func Run(scope scope.Scope, name plugin.Name, } resources = append(resources, &res) } - // Environment varables to include when invoking terraform - envs, err := parseOptionsEnvs(&options.Envs) + plugin, err := terraform.NewTerraformInstancePlugin(options, + &terraform.ImportOptions{ + InstanceSpec: importInstSpec, + Resources: resources, + }, + ) if err != nil { - log.Error("error parsing configuration Env Options", "err", err) + log.Error("error initializing pluing", "err", err) return } impls = map[run.PluginCode]interface{}{ - run.Instance: terraform.NewTerraformInstancePlugin(options.Dir, options.PollInterval.Duration(), - options.Standalone, envs, &terraform.ImportOptions{ - InstanceSpec: importInstSpec, - Resources: resources, - }), + run.Instance: plugin, } - transport.Name = name return } @@ -159,77 +111,3 @@ func mustHaveTerraform() error { } return nil } - -// parseInstanceSpecFromGroup parses the instance.Spec from the group.Spec and adds -// in the tags that should be set on the imported instance -func parseInstanceSpecFromGroup(scope scope.Scope, groupSpecURL, groupID string) (*instance.Spec, error) { - // TODO: Support a URL to a manager config with multiple nested groups - if groupSpecURL == "" { - log.Info("No group spec URL specified for import") - return nil, nil - } - var groupSpec group.Spec - t, err := scope.TemplateEngine(groupSpecURL, template.Options{MultiPass: false}) - if err != nil { - log.Error("Failed to create template", "spec", groupSpecURL, "err", err) - return nil, err - } - template, err := t.Render(nil) - if err != nil { - log.Error("Failed to render template", "spec", groupSpecURL, "err", err) - return nil, err - } - if err = types.AnyString(template).Decode(&groupSpec); err != nil { - log.Error("Failed to decode template", "spec", groupSpecURL, "err", err) - return nil, err - } - // Get the instance properties we care about - groupProps, err := group_types.ParseProperties(groupSpec) - if err != nil { - return nil, err - } - - // Add in the bootstrap tag and (if set) the group ID - tags := map[string]string{ - group.ConfigSHATag: "bootstrap", - } - // The group ID should match the spec - if groupID != "" { - if string(groupSpec.ID) != groupID { - return nil, fmt.Errorf("Given spec ID '%v' does not match given group ID '%v'", - string(groupSpec.ID), groupID) - } - tags[group.GroupTag] = groupID - } - // Use the first logical ID if set - if len(groupProps.Allocation.LogicalIDs) > 0 { - tags[instance.LogicalIDTag] = string(groupProps.Allocation.LogicalIDs[0]) - } - - spec := instance.Spec{ - Properties: groupProps.Instance.Properties, - Tags: tags, - } - log.Info("Successfully processed instance spec from group.", "group", groupSpec.ID, "spec", spec) - - return &spec, nil -} - -// parseOptionsEnvs processes the data to create a key=value slice of strings -func parseOptionsEnvs(data *types.Any) ([]string, error) { - envs := []string{} - if data == nil || len(data.Bytes()) == 0 { - return envs, nil - } - err := json.Unmarshal(data.Bytes(), &envs) - if err != nil { - return envs, fmt.Errorf("Failed to unmarshall Options.Envs data: %v", err) - } - // Must be key=value pairs - for _, val := range envs { - if !strings.Contains(val, "=") { - return []string{}, fmt.Errorf("Env var is missing '=' character: %v", val) - } - } - return envs, err -} diff --git a/pkg/run/v0/terraform/terraform_test.go b/pkg/run/v0/terraform/terraform_test.go deleted file mode 100644 index f16fb4dd5..000000000 --- a/pkg/run/v0/terraform/terraform_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package terraform - -import ( - "testing" - - "github.com/docker/infrakit/pkg/types" - "github.com/stretchr/testify/require" -) - -func TestParseOptionsEnvsInvalidJSON(t *testing.T) { - data := types.AnyString("not-json") - _, err := parseOptionsEnvs(data) - require.Error(t, err) -} - -func TestParseOptionsEnvsNil(t *testing.T) { - envs, err := parseOptionsEnvs(nil) - require.NoError(t, err) - require.Equal(t, []string{}, envs) -} - -func TestParseOptionsEnvsEmpytString(t *testing.T) { - data := types.AnyString("") - envs, err := parseOptionsEnvs(data) - require.NoError(t, err) - require.Equal(t, []string{}, envs) -} - -func TestParseOptionsEnvs(t *testing.T) { - data := types.AnyString(`["k1=v1", "k2=v2"]`) - envs, err := parseOptionsEnvs(data) - require.NoError(t, err) - require.Equal(t, []string{"k1=v1", "k2=v2"}, envs) -} - -func TestParseOptionsEnvsNotKeyValuePairs(t *testing.T) { - data := types.AnyString(`["k1=v1", "keyval"]`) - envs, err := parseOptionsEnvs(data) - require.Error(t, err) - require.Equal(t, - "Env var is missing '=' character: keyval", - err.Error()) - require.Equal(t, []string{}, envs) -}