diff --git a/go.mod b/go.mod index 8a224b5d1a..257e8e4fbe 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/terraform-providers/terraform-provider-openstack require ( github.com/gophercloud/gophercloud v0.7.1-0.20191211202411-f940f50ff1f7 - github.com/gophercloud/utils v0.0.0-20191129022341-463e26ffa30d + github.com/gophercloud/utils v0.0.0-20191212191830-4533a07bd492 github.com/hashicorp/terraform-plugin-sdk v1.1.1 github.com/mitchellh/go-homedir v1.1.0 github.com/stretchr/testify v1.3.0 diff --git a/go.sum b/go.sum index 8ede1690b8..f64b1e8159 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/gophercloud/gophercloud v0.0.0-20190212181753-892256c46858/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gophercloud/gophercloud v0.7.1-0.20191211202411-f940f50ff1f7 h1:iYD//82P8YSpKrCUaYhLMoxnIXmT5qvNNKTy5iGgc30= github.com/gophercloud/gophercloud v0.7.1-0.20191211202411-f940f50ff1f7/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss= -github.com/gophercloud/utils v0.0.0-20191129022341-463e26ffa30d h1:lHwkWOlNjHUNDVdoOacrVD/UKqLn/xsEGyD8JBATv9Y= -github.com/gophercloud/utils v0.0.0-20191129022341-463e26ffa30d/go.mod h1:SZ9FTKibIotDtCrxAU/evccoyu1yhKST6hgBvwTB5Eg= +github.com/gophercloud/utils v0.0.0-20191212191830-4533a07bd492 h1:NAwq2GgRiqbNLw1cA7KUdt7lDR/NzJtk4EXGxO3gqas= +github.com/gophercloud/utils v0.0.0-20191212191830-4533a07bd492/go.mod h1:SZ9FTKibIotDtCrxAU/evccoyu1yhKST6hgBvwTB5Eg= github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/openstack/containerinfra_shared_v1.go b/openstack/containerinfra_shared_v1.go index 12511e23b6..fa06a34fbc 100644 --- a/openstack/containerinfra_shared_v1.go +++ b/openstack/containerinfra_shared_v1.go @@ -111,7 +111,7 @@ func containerInfraClusterV1MasterFlavor(d *schema.ResourceData) (string, error) return flavor, nil } - // Try the OS_MAGNUM_FLAVOR environment variable + // Try the OS_MAGNUM_MASTER_FLAVOR environment variable if v := os.Getenv("OS_MAGNUM_MASTER_FLAVOR"); v != "" { return v, nil } diff --git a/openstack/import_openstack_orchestration_stack_v1_test.go b/openstack/import_openstack_orchestration_stack_v1_test.go new file mode 100644 index 0000000000..97e8b9323a --- /dev/null +++ b/openstack/import_openstack_orchestration_stack_v1_test.go @@ -0,0 +1,31 @@ +package openstack + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccOrchestrationStackV1_importBasic(t *testing.T) { + resourceName := "openstack_orchestration_stack_v1.stack_1" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckOrchestrationV1StackDestroy, + Steps: []resource.TestStep{ + { + Config: testAccOrchestrationV1Stack_basic, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "environment_opts", + "template_opts", + }, + }, + }, + }) +} diff --git a/openstack/orchestration_stack_v1.go b/openstack/orchestration_stack_v1.go new file mode 100644 index 0000000000..aa47a843e9 --- /dev/null +++ b/openstack/orchestration_stack_v1.go @@ -0,0 +1,71 @@ +package openstack + +import ( + "fmt" + "log" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func buildTE(t map[string]interface{}) stacks.TE { + log.Printf("[DEBUG] Start to build TE structure") + te := stacks.TE{} + if t["Bin"] != nil { + te.Bin = []byte(t["Bin"].(string)) + } + if t["URL"] != nil { + te.URL = t["URL"].(string) + } + if t["Files"] != nil { + te.Files = t["Files"].(map[string]string) + } + log.Printf("[DEBUG] TE structure builded") + return te +} + +func buildTemplateOpts(d *schema.ResourceData) *stacks.Template { + log.Printf("[DEBUG] Start building TemplateOpts") + template := &stacks.Template{} + template.TE = buildTE(d.Get("template_opts").(map[string]interface{})) + log.Printf("[DEBUG] Return TemplateOpts") + return template +} + +func buildEnvironmentOpts(d *schema.ResourceData) *stacks.Environment { + log.Printf("[DEBUG] Start building EnvironmentOpts") + environment := &stacks.Environment{} + if d.Get("environment_opts") != nil { + t := d.Get("environment_opts").(map[string]interface{}) + environment.TE = buildTE(t) + log.Printf("[DEBUG] Return EnvironmentOpts") + return environment + } + return nil +} + +func orchestrationStackV1StateRefreshFunc(client *gophercloud.ServiceClient, stackID string, isdelete bool) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + log.Printf("[DEBUG] Refresh Stack status %s", stackID) + stack, err := stacks.Find(client, stackID).Extract() + if err != nil { + if _, ok := err.(gophercloud.ErrDefault404); ok && isdelete { + return stack, "DELETE_COMPLETE", nil + } + + return nil, "", err + } + + if strings.Contains(stack.Status, "FAILED") { + return stack, stack.Status, fmt.Errorf("The stack is in error status. " + + "Please check with your cloud admin or check the orchestration " + + "API logs to see why this error occurred.") + } + + return stack, stack.Status, nil + } +} diff --git a/openstack/provider.go b/openstack/provider.go index 80c861adf5..4265005456 100644 --- a/openstack/provider.go +++ b/openstack/provider.go @@ -365,6 +365,7 @@ func Provider() terraform.ResourceProvider { "openstack_objectstorage_container_v1": resourceObjectStorageContainerV1(), "openstack_objectstorage_object_v1": resourceObjectStorageObjectV1(), "openstack_objectstorage_tempurl_v1": resourceObjectstorageTempurlV1(), + "openstack_orchestration_stack_v1": resourceOrchestrationStackV1(), "openstack_vpnaas_ipsec_policy_v2": resourceIPSecPolicyV2(), "openstack_vpnaas_service_v2": resourceServiceV2(), "openstack_vpnaas_ike_policy_v2": resourceIKEPolicyV2(), diff --git a/openstack/resource_openstack_orchestration_stack_v1.go b/openstack/resource_openstack_orchestration_stack_v1.go new file mode 100644 index 0000000000..0c09af6f0f --- /dev/null +++ b/openstack/resource_openstack_orchestration_stack_v1.go @@ -0,0 +1,375 @@ +package openstack + +import ( + "fmt" + "log" + "time" + + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceOrchestrationStackV1() *schema.Resource { + return &schema.Resource{ + Create: resourceOrchestrationStackV1Create, + Read: resourceOrchestrationStackV1Read, + Update: resourceOrchestrationStackV1Update, + Delete: resourceOrchestrationStackV1Delete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(60 * time.Minute), + Update: schema.DefaultTimeout(60 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "name": { + Type: schema.TypeString, + Required: true, + }, + + "template_opts": { + Type: schema.TypeMap, + Required: true, + }, + + "disable_rollback": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + + "environment_opts": { + Type: schema.TypeMap, + Optional: true, + }, + + "parameters": { + Type: schema.TypeMap, + Optional: true, + }, + + "timeout": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + + "tags": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + // Below are schemas for stack read + "capabilities": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + ForceNew: false, + Computed: true, + }, + + "creation_time": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + + "updated_time": { + Type: schema.TypeString, + Optional: true, + ForceNew: false, + Computed: true, + }, + + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: false, + Computed: true, + }, + + "notification_topics": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + ForceNew: false, + Computed: true, + }, + + "outputs": { + Type: schema.TypeList, + Optional: true, + ForceNew: false, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "output_key": { + Type: schema.TypeString, + Required: true, + }, + "output_value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + + "status": { + Type: schema.TypeString, + Optional: true, + ForceNew: false, + Computed: true, + }, + + "status_reason": { + Type: schema.TypeString, + Optional: true, + ForceNew: false, + Computed: true, + }, + + "template_description": { + Type: schema.TypeString, + Optional: true, + ForceNew: false, + Computed: true, + }, + }, + } +} + +func resourceOrchestrationStackV1Create(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Prepare for create openstack_orchestration_stack_v1") + config := meta.(*Config) + orchestrationClient, err := config.OrchestrationV1Client(GetRegion(d, config)) + if err != nil { + return fmt.Errorf("Error creating OpenStack Orchestration client: %s", err) + } + createOpts := &stacks.CreateOpts{ + Name: d.Get("name").(string), + TemplateOpts: buildTemplateOpts(d), + } + if d.Get("disable_rollback") != nil { + disable_rollback := d.Get("disable_rollback").(bool) + createOpts.DisableRollback = &disable_rollback + } + env := buildEnvironmentOpts(d) + if env != nil { + createOpts.EnvironmentOpts = env + } + if d.Get("parameters") != nil { + createOpts.Parameters = d.Get("parameters").(map[string]interface{}) + } + if d.Get("tags") != nil { + t := d.Get("tags").([]interface{}) + tags := make([]string, len(t)) + for _, tag := range t { + tags = append(tags, tag.(string)) + } + createOpts.Tags = tags + } + if d.Get("timeout") != nil { + createOpts.Timeout = d.Get("timeout").(int) + } + + log.Printf("[DEBUG] Creating openstack_orchestration_stack_v1") + stack, err := stacks.Create(orchestrationClient, createOpts).Extract() + if err != nil { + log.Printf("[DEBUG] openstack_orchestration_stack_v1 error occured during create: %s", err) + return fmt.Errorf("Error creating openstack_orchestration_stack_v1: %s", err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"CREATE_IN_PROGRESS", "INIT_COMPLETE"}, + Target: []string{"CREATE_COMPLETE", "UPDATE_COMPLETE", "UPDATE_IN_PROGRESS"}, + Refresh: orchestrationStackV1StateRefreshFunc(orchestrationClient, stack.ID, false), + Timeout: d.Timeout(schema.TimeoutCreate), + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf( + "Error waiting for openstack_orchestration_stack_v1 %s to become ready: %s", stack.ID, err) + } + + // Store the ID now + d.SetId(stack.ID) + log.Printf("[INFO] openstack_orchestration_stack_v1 %s create complete", stack.ID) + + return resourceOrchestrationStackV1Read(d, meta) +} + +func resourceOrchestrationStackV1Read(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + orchestrationClient, err := config.OrchestrationV1Client(GetRegion(d, config)) + if err != nil { + return fmt.Errorf("Error creating OpenStack Orchestration client: %s", err) + } + + log.Printf("[DEBUG] Fetch openstack_orchestration_stack_v1 information: %s", d.Id()) + stack, err := stacks.Find(orchestrationClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "Error retrieving openstack_orchestration_stack_v1") + } + + d.Set("name", stack.Name) + d.Set("capabilities", stack.Capabilities) + d.Set("description", stack.Description) + d.Set("disable_rollback", stack.DisableRollback) + d.Set("notification_topics", stack.NotificationTopics) + d.Set("status", stack.Status) + d.Set("status_reason", stack.StatusReason) + d.Set("template_description", stack.TemplateDescription) + d.Set("timeout", stack.Timeout) + + // Set the outputs + var outputs []map[string]interface{} + for _, o := range stack.Outputs { + output := make(map[string]interface{}) + output["description"] = o["description"] + output["output_key"] = o["output_key"] + output["output_value"] = o["output_value"] + + outputs = append(outputs, output) + } + d.Set("outputs", outputs) + + params := stack.Parameters + if stack.Parameters != nil { + remove_list := []string{"OS::project_id", "OS::stack_id", "OS::stack_name"} + for _, v := range remove_list { + _, ok := params[v] + if ok { + delete(params, v) + } + } + d.Set("parameters", stack.Parameters) + } + + if len(stack.Tags) > 0 { + var tags = []string{} + for _, v := range stack.Tags { + if v != "" { + tags = append(tags, v) + } + } + d.Set("tags", tags) + } + + if err := d.Set("creation_time", stack.CreationTime.Format(time.RFC3339)); err != nil { + log.Printf("[DEBUG] Unable to set openstack_orchestration_stack_v1 creation_time: %s", err) + } + if err := d.Set("updated_time", stack.UpdatedTime.Format(time.RFC3339)); err != nil { + log.Printf("[DEBUG] Unable to set openstack_orchestration_stack_v1 updated_at: %s", err) + } + + log.Printf("[DEBUG] openstack_orchestration_stack_v1 information fetched: %s", d.Id()) + return nil +} + +func resourceOrchestrationStackV1Update(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Prepare information for update openstack_orchestration_stack_v1") + + config := meta.(*Config) + orchestrationClient, err := config.OrchestrationV1Client(GetRegion(d, config)) + if err != nil { + return fmt.Errorf("Error creating OpenStack Orchestration client: %s", err) + } + + updateOpts := &stacks.UpdateOpts{ + TemplateOpts: buildTemplateOpts(d), + } + env := buildEnvironmentOpts(d) + if env != nil { + updateOpts.EnvironmentOpts = env + } + if d.Get("parameters") != nil { + updateOpts.Parameters = d.Get("parameters").(map[string]interface{}) + } + if d.Get("timeout") != nil { + updateOpts.Timeout = d.Get("timeout").(int) + } + if d.Get("tags") != nil { + t := d.Get("tags").([]interface{}) + tags := make([]string, len(t)) + for _, tag := range t { + tags = append(tags, tag.(string)) + } + updateOpts.Tags = tags + } + + stack, err := stacks.Find(orchestrationClient, d.Id()).Extract() + if err != nil { + return fmt.Errorf("Error retrieving openstack_orchestration_stack_v1 %s before update: %s", d.Id(), err) + } + + log.Printf("[DEBUG] Updating openstack_orchestration_stack_v1") + result := stacks.Update(orchestrationClient, stack.Name, d.Id(), updateOpts) + if result.Err != nil { + return fmt.Errorf("Error updating openstack_orchestration_stack_v1 %s: %s", d.Id(), result.Err) + } + + log.Printf("[INFO] openstack_orchestration_stack_v1 %s update complete", d.Id()) + return resourceOrchestrationStackV1Read(d, meta) +} + +func resourceOrchestrationStackV1Delete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Prepare for delete openstack_orchestration_stack_v1") + config := meta.(*Config) + orchestrationClient, err := config.OrchestrationV1Client(GetRegion(d, config)) + if err != nil { + return fmt.Errorf("Error creating OpenStack Orchestration client: %s", err) + } + + stack, err := stacks.Find(orchestrationClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "Error retrieving openstack_orchestration_stack_v1") + } + + if stack.Status != "DELETE_IN_PROGRESS" { + log.Printf("[DEBUG] Deleting openstack_orchestration_stack_v1: %s", d.Id()) + if err := stacks.Delete(orchestrationClient, stack.Name, d.Id()).ExtractErr(); err != nil { + return CheckDeleted(d, err, "Error deleting openstack_orchestration_stack_v1") + } + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"DELETE_IN_PROGRESS"}, + Target: []string{"DELETE_COMPLETE"}, + Refresh: orchestrationStackV1StateRefreshFunc(orchestrationClient, d.Id(), true), + Timeout: d.Timeout(schema.TimeoutDelete), + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for openstack_orchestration_stack_v1 %s to delete: %s", d.Id(), err) + } + + log.Printf("[INFO] openstack_orchestration_stack_v1 %s delete complete", d.Id()) + return nil +} diff --git a/openstack/resource_openstack_orchestration_stack_v1_test.go b/openstack/resource_openstack_orchestration_stack_v1_test.go new file mode 100644 index 0000000000..eb8eee65c9 --- /dev/null +++ b/openstack/resource_openstack_orchestration_stack_v1_test.go @@ -0,0 +1,282 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccOrchestrationV1Stack_basic(t *testing.T) { + var stack stacks.RetrievedStack + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckOrchestrationV1StackDestroy, + Steps: []resource.TestStep{ + { + Config: testAccOrchestrationV1Stack_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckOrchestrationV1StackExists("openstack_orchestration_stack_v1.stack_1", &stack), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_1", "name", "stack_1"), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_1", "parameters.length", "4"), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_1", "timeout", "30"), + ), + }, + }, + }) +} + +func TestAccOrchestrationV1Stack_tags(t *testing.T) { + var stack stacks.RetrievedStack + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckOrchestrationV1StackDestroy, + Steps: []resource.TestStep{ + { + Config: testAccOrchestrationV1Stack_tags, + Check: resource.ComposeTestCheckFunc( + testAccCheckOrchestrationV1StackExists("openstack_orchestration_stack_v1.stack_4", &stack), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_4", "name", "stack_4"), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_4", "tags.#", "2"), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_4", "tags.0", "foo"), + ), + }, + }, + }) +} + +func TestAccOrchestrationV1Stack_update(t *testing.T) { + var stack stacks.RetrievedStack + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckOrchestrationV1StackDestroy, + Steps: []resource.TestStep{ + { + Config: testAccOrchestrationV1Stack_preupdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckOrchestrationV1StackExists("openstack_orchestration_stack_v1.stack_3", &stack), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_3", "name", "stack_3"), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_3", "parameters.length", "4"), + ), + }, + { + Config: testAccOrchestrationV1Stack_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckOrchestrationV1StackExists("openstack_orchestration_stack_v1.stack_3", &stack), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_3", "name", "stack_3"), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_3", "parameters.length", "5"), + resource.TestCheckResourceAttrSet("openstack_orchestration_stack_v1.stack_3", "updated_time"), + ), + }, + }, + }) +} + +func TestAccOrchestrationV1Stack_timeout(t *testing.T) { + var stack stacks.RetrievedStack + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckOrchestrationV1StackDestroy, + Steps: []resource.TestStep{ + { + Config: testAccOrchestrationV1Stack_timeout, + Check: resource.ComposeTestCheckFunc( + testAccCheckOrchestrationV1StackExists("openstack_orchestration_stack_v1.stack_2", &stack), + ), + }, + }, + }) +} + +func TestAccOrchestrationV1Stack_outputs(t *testing.T) { + var stack stacks.RetrievedStack + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckOrchestrationV1StackDestroy, + Steps: []resource.TestStep{ + { + Config: testAccOrchestrationV1Stack_outputs, + Check: resource.ComposeTestCheckFunc( + testAccCheckOrchestrationV1StackExists("openstack_orchestration_stack_v1.stack_5", &stack), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_5", "name", "stack_5"), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_5", "outputs.#", "1"), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_5", "outputs.0.output_value", "foo"), + resource.TestCheckResourceAttr("openstack_orchestration_stack_v1.stack_5", "outputs.0.output_key", "value1"), + ), + }, + }, + }) +} + +func testAccCheckOrchestrationV1StackDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + orchestrationClient, err := config.OrchestrationV1Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating OpenStack Orchestration client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_orchestration_stack_v1" { + continue + } + + stack, err := stacks.Find(orchestrationClient, rs.Primary.ID).Extract() + if err == nil { + if stack.Status != "DELETE_COMPLETE" { + return fmt.Errorf("stack still exists") + } + } + } + + return nil +} + +func testAccCheckOrchestrationV1StackExists(n string, stack *stacks.RetrievedStack) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + orchestrationClient, err := config.OrchestrationV1Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating OpenStack orchestration client: %s", err) + } + + found, err := stacks.Find(orchestrationClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Stack not found") + } + + *stack = *found + + return nil + } +} + +const testAccOrchestrationV1Stack_basic = ` +resource "openstack_orchestration_stack_v1" "stack_1" { + name = "stack_1" + parameters = { + length = 4 + } + template_opts = { + Bin = "heat_template_version: 2013-05-23\nparameters:\n length:\n type: number\nresources:\n test_res:\n type: OS::Heat::TestResource\n random:\n type: OS::Heat::RandomString\n properties:\n length: {get_param: length}\n" + } + environment_opts = { + Bin = "\n" + } + disable_rollback = true + timeout = 30 +} +` + +const testAccOrchestrationV1Stack_preupdate = ` +resource "openstack_orchestration_stack_v1" "stack_3" { + name = "stack_3" + parameters = { + length = 4 + } + template_opts = { + Bin = "heat_template_version: 2013-05-23\nparameters:\n length:\n type: number\nresources:\n test_res:\n type: OS::Heat::TestResource\n random:\n type: OS::Heat::RandomString\n properties:\n length: {get_param: length}\n" + } + environment_opts = { + Bin = "\n" + } + disable_rollback = true +} +` + +const testAccOrchestrationV1Stack_update = ` +resource "openstack_orchestration_stack_v1" "stack_3" { + name = "stack_3" + parameters = { + length = 5 + } + template_opts = { + Bin = "heat_template_version: 2013-05-23\nparameters:\n length:\n type: number\nresources:\n test_res:\n type: OS::Heat::TestResource\n random:\n type: OS::Heat::RandomString\n properties:\n length: {get_param: length}\n" + } + environment_opts = { + Bin = "\n" + } + disable_rollback = true +} +` + +const testAccOrchestrationV1Stack_timeout = ` +resource "openstack_orchestration_stack_v1" "stack_2" { + name = "stack_2" + parameters = { + length = 4 + } + template_opts = { + Bin = "heat_template_version: 2013-05-23\nparameters:\n length:\n type: number\nresources:\n test_res:\n type: OS::Heat::TestResource\n random:\n type: OS::Heat::RandomString\n properties:\n length: {get_param: length}\n" + } + environment_opts = { + Bin = "\n" + } + disable_rollback = true + timeouts { + create = "5m" + update = "5m" + delete = "5m" + } +} +` + +const testAccOrchestrationV1Stack_tags = ` +resource "openstack_orchestration_stack_v1" "stack_4" { + name = "stack_4" + parameters = { + length = 4 + } + template_opts = { + Bin = "heat_template_version: 2013-05-23\nparameters:\n length:\n type: number\nresources:\n test_res:\n type: OS::Heat::TestResource\n random:\n type: OS::Heat::RandomString\n properties:\n length: {get_param: length}\n" + } + environment_opts = { + Bin = "\n" + } + disable_rollback = true + tags = [ + "foo", + "bar", + ] +} +` + +const testAccOrchestrationV1Stack_outputs = ` +resource "openstack_orchestration_stack_v1" "stack_5" { + name = "stack_5" + parameters = { + length = 4 + } + template_opts = { + Bin = "heat_template_version: 2013-05-23\nparameters:\n length:\n type: number\nresources:\n test_res:\n type: OS::Heat::TestResource\n random:\n type: OS::Heat::RandomString\n properties:\n length: {get_param: length}\noutputs:\n value1:\n value: foo" + } + environment_opts = { + Bin = "\n" + } + disable_rollback = true +} +` diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/doc.go new file mode 100644 index 0000000000..33fc3271c4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/doc.go @@ -0,0 +1,239 @@ +/* +Package stacks provides operation for working with Heat stacks. A stack is a +group of resources (servers, load balancers, databases, and so forth) +combined to fulfill a useful purpose. Based on a template, Heat orchestration +engine creates an instantiated set of resources (a stack) to run the +application framework or component specified (in the template). A stack is a +running instance of a template. The result of creating a stack is a deployment +of the application framework or component. + +Prepare required import packages + +import ( + "fmt" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks" +) + +Example of Preparing Orchestration client: + + client, err := openstack.NewOrchestrationV1(provider, gophercloud.EndpointOpts{Region: "RegionOne"}) + +Example of List Stack: + all_stack_pages, err := stacks.List(client, nil).AllPages() + if err != nil { + panic(err) + } + + all_stacks, err := stacks.ExtractStacks(all_stack_pages) + if err != nil { + panic(err) + } + + for _, stack := range all_stacks { + fmt.Printf("%+v\n", stack) + } + + +Example to Create an Stack + + // Create Template + t := make(map[string]interface{}) + f, err := ioutil.ReadFile("template.yaml") + if err != nil { + panic(err) + } + err = yaml.Unmarshal(f, t) + if err != nil { + panic(err) + } + + template := &stacks.Template{} + template.TE = stacks.TE{ + Bin: f, + } + // Create Environment if needed + t_env := make(map[string]interface{}) + f_env, err := ioutil.ReadFile("env.yaml") + if err != nil { + panic(err) + } + err = yaml.Unmarshal(f_env, t_env) + if err != nil { + panic(err) + } + + env := &stacks.Environment{} + env.TE = stacks.TE{ + Bin: f_env, + } + + // Remember, the priority of parameters you given through + // Parameters is higher than the parameters you provided in EnvironmentOpts. + params := make(map[string]string) + params["number_of_nodes"] = 1 + tags := []string{"example-stack"} + createOpts := &stacks.CreateOpts{ + // The name of the stack. It must start with an alphabetic character. + Name: "testing_group", + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + TemplateOpts: template, + // A structure that contains details for the environment of the stack. + EnvironmentOpts: env, + // User-defined parameters to pass to the template. + Parameters: params, + // A list of tags to assosciate with the Stack + Tags: tags, + } + + r := stacks.Create(client, createOpts) + //dcreated_stack := stacks.CreatedStack() + if r.Err != nil { + panic(r.Err) + } + created_stack, err := r.Extract() + if err != nil { + panic(err) + } + fmt.Printf("Created Stack: %v", created_stack.ID) + +Example for Get Stack + + get_result := stacks.Get(client, stackName, created_stack.ID) + if get_result.Err != nil { + panic(get_result.Err) + } + stack, err := get_result.Extract() + if err != nil { + panic(err) + } + fmt.Println("Get Stack: Name: ", stack.Name, ", ID: ", stack.ID, ", Status: ", stack.Status) + +Example for Find Stack + + find_result := stacks.Find(client, stackIdentity) + if find_result.Err != nil { + panic(find_result.Err) + } + stack, err := find_result.Extract() + if err != nil { + panic(err) + } + fmt.Println("Find Stack: Name: ", stack.Name, ", ID: ", stack.ID, ", Status: ", stack.Status) + +Example for Delete Stack + + del_r := stacks.Delete(client, stackName, created_stack.ID) + if del_r.Err != nil { + panic(del_r.Err) + } + fmt.Println("Deleted Stack: ", stackName) + +Summary of Behavior Between Stack Update and UpdatePatch Methods : + +Function | Test Case | Result + +Update() | Template AND Parameters WITH Conflict | Parameter takes priority, parameters are set in raw_template.environment overlay +Update() | Template ONLY | Template updates, raw_template.environment overlay is removed +Update() | Parameters ONLY | No update, template is required + +UpdatePatch() | Template AND Parameters WITH Conflict | Parameter takes priority, parameters are set in raw_template.environment overlay +UpdatePatch() | Template ONLY | Template updates, but raw_template.environment overlay is not removed, existing parameter values will remain +UpdatePatch() | Parameters ONLY | Parameters (raw_template.environment) is updated, excluded values are unchanged + +The PUT Update() function will remove parameters from the raw_template.environment overlay +if they are excluded from the operation, whereas PATCH Update() will never be destructive to the +raw_template.environment overlay. It is not possible to expose the raw_template values with a +patch update once they have been added to the environment overlay with the PATCH verb, but +newly added values that do not have a corresponding key in the overlay will display the +raw_template value. + +Example to Update a Stack Using the Update (PUT) Method + + t := make(map[string]interface{}) + f, err := ioutil.ReadFile("template.yaml") + if err != nil { + panic(err) + } + err = yaml.Unmarshal(f, t) + if err != nil { + panic(err) + } + + template := stacks.Template{} + template.TE = stacks.TE{ + Bin: f, + } + + var params = make(map[string]interface{}) + params["number_of_nodes"] = 2 + + stackName := "my_stack" + stackId := "d68cc349-ccc5-4b44-a17d-07f068c01e5a" + + stackOpts := &stacks.UpdateOpts{ + Parameters: params, + TemplateOpts: &template, + } + + res := stacks.Update(orchestrationClient, stackName, stackId, stackOpts) + if res.Err != nil { + panic(res.Err) + } + +Example to Update a Stack Using the UpdatePatch (PATCH) Method + + var params = make(map[string]interface{}) + params["number_of_nodes"] = 2 + + stackName := "my_stack" + stackId := "d68cc349-ccc5-4b44-a17d-07f068c01e5a" + + stackOpts := &stacks.UpdateOpts{ + Parameters: params, + } + + res := stacks.UpdatePatch(orchestrationClient, stackName, stackId, stackOpts) + if res.Err != nil { + panic(res.Err) + } + +Example YAML Template Containing a Heat::ResourceGroup With Three Nodes + + heat_template_version: 2016-04-08 + + parameters: + number_of_nodes: + type: number + default: 3 + description: the number of nodes + node_flavor: + type: string + default: m1.small + description: node flavor + node_image: + type: string + default: centos7.5-latest + description: node os image + node_network: + type: string + default: my-node-network + description: node network name + + resources: + resource_group: + type: OS::Heat::ResourceGroup + properties: + count: { get_param: number_of_nodes } + resource_def: + type: OS::Nova::Server + properties: + name: my_nova_server_%index% + image: { get_param: node_image } + flavor: { get_param: node_flavor } + networks: + - network: {get_param: node_network} +*/ +package stacks diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/environment.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/environment.go new file mode 100644 index 0000000000..86989186fa --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/environment.go @@ -0,0 +1,134 @@ +package stacks + +import "strings" + +// Environment is a structure that represents stack environments +type Environment struct { + TE +} + +// EnvironmentSections is a map containing allowed sections in a stack environment file +var EnvironmentSections = map[string]bool{ + "parameters": true, + "parameter_defaults": true, + "resource_registry": true, +} + +// Validate validates the contents of the Environment +func (e *Environment) Validate() error { + if e.Parsed == nil { + if err := e.Parse(); err != nil { + return err + } + } + for key := range e.Parsed { + if _, ok := EnvironmentSections[key]; !ok { + return ErrInvalidEnvironment{Section: key} + } + } + return nil +} + +// Parse environment file to resolve the URL's of the resources. This is done by +// reading from the `Resource Registry` section, which is why the function is +// named GetRRFileContents. +func (e *Environment) getRRFileContents(ignoreIf igFunc) error { + // initialize environment if empty + if e.Files == nil { + e.Files = make(map[string]string) + } + if e.fileMaps == nil { + e.fileMaps = make(map[string]string) + } + + // get the resource registry + rr := e.Parsed["resource_registry"] + + // search the resource registry for URLs + switch rr.(type) { + // process further only if the resource registry is a map + case map[string]interface{}, map[interface{}]interface{}: + rrMap, err := toStringKeys(rr) + if err != nil { + return err + } + // the resource registry might contain a base URL for the resource. If + // such a field is present, use it. Otherwise, use the default base URL. + var baseURL string + if val, ok := rrMap["base_url"]; ok { + baseURL = val.(string) + } else { + baseURL = e.baseURL + } + + // The contents of the resource may be located in a remote file, which + // will be a template. Instantiate a temporary template to manage the + // contents. + tempTemplate := new(Template) + tempTemplate.baseURL = baseURL + tempTemplate.client = e.client + + // Fetch the contents of remote resource URL's + if err = tempTemplate.getFileContents(rr, ignoreIf, false); err != nil { + return err + } + // check the `resources` section (if it exists) for more URL's. Note that + // the previous call to GetFileContents was (deliberately) not recursive + // as we want more control over where to look for URL's + if val, ok := rrMap["resources"]; ok { + switch val.(type) { + // process further only if the contents are a map + case map[string]interface{}, map[interface{}]interface{}: + resourcesMap, err := toStringKeys(val) + if err != nil { + return err + } + for _, v := range resourcesMap { + switch v.(type) { + case map[string]interface{}, map[interface{}]interface{}: + resourceMap, err := toStringKeys(v) + if err != nil { + return err + } + var resourceBaseURL string + // if base_url for the resource type is defined, use it + if val, ok := resourceMap["base_url"]; ok { + resourceBaseURL = val.(string) + } else { + resourceBaseURL = baseURL + } + tempTemplate.baseURL = resourceBaseURL + if err := tempTemplate.getFileContents(v, ignoreIf, false); err != nil { + return err + } + } + } + } + } + // if the resource registry contained any URL's, store them. This can + // then be passed as parameter to api calls to Heat api. + e.Files = tempTemplate.Files + return nil + default: + return nil + } +} + +// function to choose keys whose values are other environment files +func ignoreIfEnvironment(key string, value interface{}) bool { + // base_url and hooks refer to components which cannot have urls + if key == "base_url" || key == "hooks" { + return true + } + // if value is not string, it cannot be a URL + valueString, ok := value.(string) + if !ok { + return true + } + // if value contains `::`, it must be a reference to another resource type + // e.g. OS::Nova::Server : Rackspace::Cloud::Server + if strings.Contains(valueString, "::") { + return true + } + return false +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/errors.go new file mode 100644 index 0000000000..a6febe0408 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/errors.go @@ -0,0 +1,41 @@ +package stacks + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" +) + +type ErrInvalidEnvironment struct { + gophercloud.BaseError + Section string +} + +func (e ErrInvalidEnvironment) Error() string { + return fmt.Sprintf("Environment has wrong section: %s", e.Section) +} + +type ErrInvalidDataFormat struct { + gophercloud.BaseError +} + +func (e ErrInvalidDataFormat) Error() string { + return fmt.Sprintf("Data in neither json nor yaml format.") +} + +type ErrInvalidTemplateFormatVersion struct { + gophercloud.BaseError + Version string +} + +func (e ErrInvalidTemplateFormatVersion) Error() string { + return fmt.Sprintf("Template format version not found.") +} + +type ErrTemplateRequired struct { + gophercloud.BaseError +} + +func (e ErrTemplateRequired) Error() string { + return fmt.Sprintf("Template required for this function.") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/fixtures.go new file mode 100644 index 0000000000..58987d4bfd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/fixtures.go @@ -0,0 +1,199 @@ +package stacks + +// ValidJSONTemplate is a valid OpenStack Heat template in JSON format +const ValidJSONTemplate = ` +{ + "heat_template_version": "2014-10-16", + "parameters": { + "flavor": { + "default": "debian2G", + "description": "Flavor for the server to be created", + "hidden": true, + "type": "string" + } + }, + "resources": { + "test_server": { + "properties": { + "flavor": "2 GB General Purpose v1", + "image": "Debian 7 (Wheezy) (PVHVM)", + "name": "test-server" + }, + "type": "OS::Nova::Server" + } + } +} +` + +// ValidYAMLTemplate is a valid OpenStack Heat template in YAML format +const ValidYAMLTemplate = ` +heat_template_version: 2014-10-16 +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: debian2G + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) +` + +// InvalidTemplateNoVersion is an invalid template as it has no `version` section +const InvalidTemplateNoVersion = ` +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: debian2G + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) +` + +// ValidJSONEnvironment is a valid environment for a stack in JSON format +const ValidJSONEnvironment = ` +{ + "parameters": { + "user_key": "userkey" + }, + "resource_registry": { + "My::WP::Server": "file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml", + "OS::Quantum*": "OS::Neutron*", + "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml", + "OS::Metering::Alarm": "OS::Ceilometer::Alarm", + "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml", + "resources": { + "my_db_server": { + "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml" + }, + "my_server": { + "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml", + "hooks": "pre-create" + }, + "nested_stack": { + "nested_resource": { + "hooks": "pre-update" + }, + "another_resource": { + "hooks": [ + "pre-create", + "pre-update" + ] + } + } + } + } +} +` + +// ValidYAMLEnvironment is a valid environment for a stack in YAML format +const ValidYAMLEnvironment = ` +parameters: + user_key: userkey +resource_registry: + My::WP::Server: file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml + # allow older templates with Quantum in them. + "OS::Quantum*": "OS::Neutron*" + # Choose your implementation of AWS::CloudWatch::Alarm + "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml" + #"AWS::CloudWatch::Alarm": "OS::Heat::CWLiteAlarm" + "OS::Metering::Alarm": "OS::Ceilometer::Alarm" + "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml" + resources: + my_db_server: + "OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml + my_server: + "OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml + hooks: pre-create + nested_stack: + nested_resource: + hooks: pre-update + another_resource: + hooks: [pre-create, pre-update] +` + +// InvalidEnvironment is an invalid environment as it has an extra section called `resources` +const InvalidEnvironment = ` +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: debian2G + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) +parameter_defaults: + KeyName: heat_key +` + +// ValidJSONEnvironmentParsed is the expected parsed version of ValidJSONEnvironment +var ValidJSONEnvironmentParsed = map[string]interface{}{ + "parameters": map[string]interface{}{ + "user_key": "userkey", + }, + "resource_registry": map[string]interface{}{ + "My::WP::Server": "file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml", + "OS::Quantum*": "OS::Neutron*", + "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml", + "OS::Metering::Alarm": "OS::Ceilometer::Alarm", + "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml", + "resources": map[string]interface{}{ + "my_db_server": map[string]interface{}{ + "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml", + }, + "my_server": map[string]interface{}{ + "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml", + "hooks": "pre-create", + }, + "nested_stack": map[string]interface{}{ + "nested_resource": map[string]interface{}{ + "hooks": "pre-update", + }, + "another_resource": map[string]interface{}{ + "hooks": []interface{}{ + "pre-create", + "pre-update", + }, + }, + }, + }, + }, +} + +// ValidJSONTemplateParsed is the expected parsed version of ValidJSONTemplate +var ValidJSONTemplateParsed = map[string]interface{}{ + "heat_template_version": "2014-10-16", + "parameters": map[string]interface{}{ + "flavor": map[string]interface{}{ + "default": "debian2G", + "description": "Flavor for the server to be created", + "hidden": true, + "type": "string", + }, + }, + "resources": map[string]interface{}{ + "test_server": map[string]interface{}{ + "properties": map[string]interface{}{ + "flavor": "2 GB General Purpose v1", + "image": "Debian 7 (Wheezy) (PVHVM)", + "name": "test-server", + }, + "type": "OS::Nova::Server", + }, + }, +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/requests.go new file mode 100644 index 0000000000..d7fd4f16fd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/requests.go @@ -0,0 +1,520 @@ +package stacks + +import ( + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToStackCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // The name of the stack. It must start with an alphabetic character. + Name string `json:"stack_name" required:"true"` + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + TemplateOpts *Template `json:"-" required:"true"` + // Enables or disables deletion of all stack resources when a stack + // creation fails. Default is true, meaning all resources are not deleted when + // stack creation fails. + DisableRollback *bool `json:"disable_rollback,omitempty"` + // A structure that contains details for the environment of the stack. + EnvironmentOpts *Environment `json:"-"` + // User-defined parameters to pass to the template. + Parameters map[string]interface{} `json:"parameters,omitempty"` + // The timeout for stack creation in minutes. + Timeout int `json:"timeout_mins,omitempty"` + // A list of tags to assosciate with the Stack + Tags []string `json:"-"` +} + +// ToStackCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToStackCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if err := opts.TemplateOpts.Parse(); err != nil { + return nil, err + } + + if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil { + return nil, err + } + opts.TemplateOpts.fixFileRefs() + b["template"] = string(opts.TemplateOpts.Bin) + + files := make(map[string]string) + for k, v := range opts.TemplateOpts.Files { + files[k] = v + } + + if opts.EnvironmentOpts != nil { + if err := opts.EnvironmentOpts.Parse(); err != nil { + return nil, err + } + if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil { + return nil, err + } + opts.EnvironmentOpts.fixFileRefs() + for k, v := range opts.EnvironmentOpts.Files { + files[k] = v + } + b["environment"] = string(opts.EnvironmentOpts.Bin) + } + + if len(files) > 0 { + b["files"] = files + } + + if opts.Tags != nil { + b["tags"] = strings.Join(opts.Tags, ",") + } + + return b, nil +} + +// Create accepts a CreateOpts struct and creates a new stack using the values +// provided. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToStackCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// AdoptOptsBuilder is the interface options structs have to satisfy in order +// to be used in the Adopt function in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type AdoptOptsBuilder interface { + ToStackAdoptMap() (map[string]interface{}, error) +} + +// AdoptOpts is the common options struct used in this package's Adopt +// operation. +type AdoptOpts struct { + // Existing resources data represented as a string to add to the + // new stack. Data returned by Abandon could be provided as AdoptsStackData. + AdoptStackData string `json:"adopt_stack_data" required:"true"` + // The name of the stack. It must start with an alphabetic character. + Name string `json:"stack_name" required:"true"` + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + TemplateOpts *Template `json:"-" required:"true"` + // The timeout for stack creation in minutes. + Timeout int `json:"timeout_mins,omitempty"` + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + //TemplateOpts *Template `json:"-" required:"true"` + // Enables or disables deletion of all stack resources when a stack + // creation fails. Default is true, meaning all resources are not deleted when + // stack creation fails. + DisableRollback *bool `json:"disable_rollback,omitempty"` + // A structure that contains details for the environment of the stack. + EnvironmentOpts *Environment `json:"-"` + // User-defined parameters to pass to the template. + Parameters map[string]interface{} `json:"parameters,omitempty"` +} + +// ToStackAdoptMap casts a CreateOpts struct to a map. +func (opts AdoptOpts) ToStackAdoptMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if err := opts.TemplateOpts.Parse(); err != nil { + return nil, err + } + + if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil { + return nil, err + } + opts.TemplateOpts.fixFileRefs() + b["template"] = string(opts.TemplateOpts.Bin) + + files := make(map[string]string) + for k, v := range opts.TemplateOpts.Files { + files[k] = v + } + + if opts.EnvironmentOpts != nil { + if err := opts.EnvironmentOpts.Parse(); err != nil { + return nil, err + } + if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil { + return nil, err + } + opts.EnvironmentOpts.fixFileRefs() + for k, v := range opts.EnvironmentOpts.Files { + files[k] = v + } + b["environment"] = string(opts.EnvironmentOpts.Bin) + } + + if len(files) > 0 { + b["files"] = files + } + + return b, nil +} + +// Adopt accepts an AdoptOpts struct and creates a new stack using the resources +// from another stack. +func Adopt(c *gophercloud.ServiceClient, opts AdoptOptsBuilder) (r AdoptResult) { + b, err := opts.ToStackAdoptMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(adoptURL(c), b, &r.Body, nil) + return +} + +// SortDir is a type for specifying in which direction to sort a list of stacks. +type SortDir string + +// SortKey is a type for specifying by which key to sort a list of stacks. +type SortKey string + +var ( + // SortAsc is used to sort a list of stacks in ascending order. + SortAsc SortDir = "asc" + // SortDesc is used to sort a list of stacks in descending order. + SortDesc SortDir = "desc" + // SortName is used to sort a list of stacks by name. + SortName SortKey = "name" + // SortStatus is used to sort a list of stacks by status. + SortStatus SortKey = "status" + // SortCreatedAt is used to sort a list of stacks by date created. + SortCreatedAt SortKey = "created_at" + // SortUpdatedAt is used to sort a list of stacks by date updated. + SortUpdatedAt SortKey = "updated_at" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToStackListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the network attributes you want to see returned. +type ListOpts struct { + // TenantID is the UUID of the tenant. A tenant is also known as + // a project. + TenantID string `q:"tenant_id"` + + // ID filters the stack list by a stack ID + ID string `q:"id"` + + // Status filters the stack list by a status. + Status string `q:"status"` + + // Name filters the stack list by a name. + Name string `q:"name"` + + // Marker is the ID of last-seen item. + Marker string `q:"marker"` + + // Limit is an integer value for the limit of values to return. + Limit int `q:"limit"` + + // SortKey allows you to sort by stack_name, stack_status, creation_time, or + // update_time key. + SortKey SortKey `q:"sort_keys"` + + // SortDir sets the direction, and is either `asc` or `desc`. + SortDir SortDir `q:"sort_dir"` + + // AllTenants is a bool to show all tenants. + AllTenants bool `q:"global_tenant"` + + // ShowDeleted set to `true` to include deleted stacks in the list. + ShowDeleted bool `q:"show_deleted"` + + // ShowNested set to `true` to include nested stacks in the list. + ShowNested bool `q:"show_nested"` + + // Tags lists stacks that contain one or more simple string tags. + Tags string `q:"tags"` + + // TagsAny lists stacks that contain one or more simple string tags. + TagsAny string `q:"tags_any"` + + // NotTags lists stacks that do not contain one or more simple string tags. + NotTags string `q:"not_tags"` + + // NotTagsAny lists stacks that do not contain one or more simple string tags. + NotTagsAny string `q:"not_tags_any"` +} + +// ToStackListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToStackListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// stacks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToStackListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + createPage := func(r pagination.PageResult) pagination.Page { + return StackPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(c, url, createPage) +} + +// Get retreives a stack based on the stack name and stack ID. +func Get(c *gophercloud.ServiceClient, stackName, stackID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, stackName, stackID), &r.Body, nil) + return +} + +// Find retrieves a stack based on the stack name or stack ID. +func Find(c *gophercloud.ServiceClient, stackIdentity string) (r GetResult) { + _, r.Err = c.Get(findURL(c, stackIdentity), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the Update operation in this package. +type UpdateOptsBuilder interface { + ToStackUpdateMap() (map[string]interface{}, error) +} + +// UpdatePatchOptsBuilder is the interface options structs have to satisfy in order +// to be used in the UpdatePatch operation in this package +type UpdatePatchOptsBuilder interface { + ToStackUpdatePatchMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the common options struct used in this package's Update +// and UpdatePatch operations. +type UpdateOpts struct { + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + TemplateOpts *Template `json:"-"` + // A structure that contains details for the environment of the stack. + EnvironmentOpts *Environment `json:"-"` + // User-defined parameters to pass to the template. + Parameters map[string]interface{} `json:"parameters,omitempty"` + // The timeout for stack creation in minutes. + Timeout int `json:"timeout_mins,omitempty"` + // A list of tags to associate with the Stack + Tags []string `json:"-"` +} + +// ToStackUpdateMap validates that a template was supplied and calls +// the toStackUpdateMap private function. +func (opts UpdateOpts) ToStackUpdateMap() (map[string]interface{}, error) { + if opts.TemplateOpts == nil { + return nil, ErrTemplateRequired{} + } + return toStackUpdateMap(opts) +} + +// ToStackUpdatePatchMap calls the private function toStackUpdateMap +// directly. +func (opts UpdateOpts) ToStackUpdatePatchMap() (map[string]interface{}, error) { + return toStackUpdateMap(opts) +} + +// ToStackUpdateMap casts a CreateOpts struct to a map. +func toStackUpdateMap(opts UpdateOpts) (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + files := make(map[string]string) + + if opts.TemplateOpts != nil { + if err := opts.TemplateOpts.Parse(); err != nil { + return nil, err + } + + if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil { + return nil, err + } + opts.TemplateOpts.fixFileRefs() + b["template"] = string(opts.TemplateOpts.Bin) + + for k, v := range opts.TemplateOpts.Files { + files[k] = v + } + } + + if opts.EnvironmentOpts != nil { + if err := opts.EnvironmentOpts.Parse(); err != nil { + return nil, err + } + if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil { + return nil, err + } + opts.EnvironmentOpts.fixFileRefs() + for k, v := range opts.EnvironmentOpts.Files { + files[k] = v + } + b["environment"] = string(opts.EnvironmentOpts.Bin) + } + + if len(files) > 0 { + b["files"] = files + } + + if opts.Tags != nil { + b["tags"] = strings.Join(opts.Tags, ",") + } + + return b, nil +} + +// Update accepts an UpdateOpts struct and updates an existing stack using the +// http PUT verb with the values provided. opts.TemplateOpts is required. +func Update(c *gophercloud.ServiceClient, stackName, stackID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToStackUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, stackName, stackID), b, nil, nil) + return +} + +// Update accepts an UpdateOpts struct and updates an existing stack using the +// http PATCH verb with the values provided. opts.TemplateOpts is not required. +func UpdatePatch(c *gophercloud.ServiceClient, stackName, stackID string, opts UpdatePatchOptsBuilder) (r UpdateResult) { + b, err := opts.ToStackUpdatePatchMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Patch(updateURL(c, stackName, stackID), b, nil, nil) + return +} + +// Delete deletes a stack based on the stack name and stack ID. +func Delete(c *gophercloud.ServiceClient, stackName, stackID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, stackName, stackID), nil) + return +} + +// PreviewOptsBuilder is the interface options structs have to satisfy in order +// to be used in the Preview operation in this package. +type PreviewOptsBuilder interface { + ToStackPreviewMap() (map[string]interface{}, error) +} + +// PreviewOpts contains the common options struct used in this package's Preview +// operation. +type PreviewOpts struct { + // The name of the stack. It must start with an alphabetic character. + Name string `json:"stack_name" required:"true"` + // The timeout for stack creation in minutes. + Timeout int `json:"timeout_mins" required:"true"` + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + TemplateOpts *Template `json:"-" required:"true"` + // Enables or disables deletion of all stack resources when a stack + // creation fails. Default is true, meaning all resources are not deleted when + // stack creation fails. + DisableRollback *bool `json:"disable_rollback,omitempty"` + // A structure that contains details for the environment of the stack. + EnvironmentOpts *Environment `json:"-"` + // User-defined parameters to pass to the template. + Parameters map[string]interface{} `json:"parameters,omitempty"` +} + +// ToStackPreviewMap casts a PreviewOpts struct to a map. +func (opts PreviewOpts) ToStackPreviewMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if err := opts.TemplateOpts.Parse(); err != nil { + return nil, err + } + + if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil { + return nil, err + } + opts.TemplateOpts.fixFileRefs() + b["template"] = string(opts.TemplateOpts.Bin) + + files := make(map[string]string) + for k, v := range opts.TemplateOpts.Files { + files[k] = v + } + + if opts.EnvironmentOpts != nil { + if err := opts.EnvironmentOpts.Parse(); err != nil { + return nil, err + } + if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil { + return nil, err + } + opts.EnvironmentOpts.fixFileRefs() + for k, v := range opts.EnvironmentOpts.Files { + files[k] = v + } + b["environment"] = string(opts.EnvironmentOpts.Bin) + } + + if len(files) > 0 { + b["files"] = files + } + + return b, nil +} + +// Preview accepts a PreviewOptsBuilder interface and creates a preview of a stack using the values +// provided. +func Preview(c *gophercloud.ServiceClient, opts PreviewOptsBuilder) (r PreviewResult) { + b, err := opts.ToStackPreviewMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(previewURL(c), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Abandon deletes the stack with the provided stackName and stackID, but leaves its +// resources intact, and returns data describing the stack and its resources. +func Abandon(c *gophercloud.ServiceClient, stackName, stackID string) (r AbandonResult) { + _, r.Err = c.Delete(abandonURL(c, stackName, stackID), &gophercloud.RequestOpts{ + JSONResponse: &r.Body, + OkCodes: []int{200}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/results.go new file mode 100644 index 0000000000..054ab3d74b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/results.go @@ -0,0 +1,301 @@ +package stacks + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreatedStack represents the object extracted from a Create operation. +type CreatedStack struct { + ID string `json:"id"` + Links []gophercloud.Link `json:"links"` +} + +// CreateResult represents the result of a Create operation. +type CreateResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a CreatedStack object and is called after a +// Create operation. +func (r CreateResult) Extract() (*CreatedStack, error) { + var s struct { + CreatedStack *CreatedStack `json:"stack"` + } + err := r.ExtractInto(&s) + return s.CreatedStack, err +} + +// AdoptResult represents the result of an Adopt operation. AdoptResult has the +// same form as CreateResult. +type AdoptResult struct { + CreateResult +} + +// StackPage is a pagination.Pager that is returned from a call to the List function. +type StackPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Stacks. +func (r StackPage) IsEmpty() (bool, error) { + stacks, err := ExtractStacks(r) + return len(stacks) == 0, err +} + +// ListedStack represents an element in the slice extracted from a List operation. +type ListedStack struct { + CreationTime time.Time `json:"-"` + Description string `json:"description"` + ID string `json:"id"` + Links []gophercloud.Link `json:"links"` + Name string `json:"stack_name"` + Status string `json:"stack_status"` + StatusReason string `json:"stack_status_reason"` + Tags []string `json:"tags"` + UpdatedTime time.Time `json:"-"` +} + +func (r *ListedStack) UnmarshalJSON(b []byte) error { + type tmp ListedStack + var s struct { + tmp + CreationTime string `json:"creation_time"` + UpdatedTime string `json:"updated_time"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = ListedStack(s.tmp) + + if s.CreationTime != "" { + t, err := time.Parse(time.RFC3339, s.CreationTime) + if err != nil { + t, err = time.Parse(gophercloud.RFC3339NoZ, s.CreationTime) + if err != nil { + return err + } + } + r.CreationTime = t + } + + if s.UpdatedTime != "" { + t, err := time.Parse(time.RFC3339, s.UpdatedTime) + if err != nil { + t, err = time.Parse(gophercloud.RFC3339NoZ, s.UpdatedTime) + if err != nil { + return err + } + } + r.UpdatedTime = t + } + + return nil +} + +// ExtractStacks extracts and returns a slice of ListedStack. It is used while iterating +// over a stacks.List call. +func ExtractStacks(r pagination.Page) ([]ListedStack, error) { + var s struct { + ListedStacks []ListedStack `json:"stacks"` + } + err := (r.(StackPage)).ExtractInto(&s) + return s.ListedStacks, err +} + +// RetrievedStack represents the object extracted from a Get operation. +type RetrievedStack struct { + Capabilities []interface{} `json:"capabilities"` + CreationTime time.Time `json:"-"` + Description string `json:"description"` + DisableRollback bool `json:"disable_rollback"` + ID string `json:"id"` + Links []gophercloud.Link `json:"links"` + NotificationTopics []interface{} `json:"notification_topics"` + Outputs []map[string]interface{} `json:"outputs"` + Parameters map[string]string `json:"parameters"` + Name string `json:"stack_name"` + Status string `json:"stack_status"` + StatusReason string `json:"stack_status_reason"` + Tags []string `json:"tags"` + TemplateDescription string `json:"template_description"` + Timeout int `json:"timeout_mins"` + UpdatedTime time.Time `json:"-"` +} + +func (r *RetrievedStack) UnmarshalJSON(b []byte) error { + type tmp RetrievedStack + var s struct { + tmp + CreationTime string `json:"creation_time"` + UpdatedTime string `json:"updated_time"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = RetrievedStack(s.tmp) + + if s.CreationTime != "" { + t, err := time.Parse(time.RFC3339, s.CreationTime) + if err != nil { + t, err = time.Parse(gophercloud.RFC3339NoZ, s.CreationTime) + if err != nil { + return err + } + } + r.CreationTime = t + } + + if s.UpdatedTime != "" { + t, err := time.Parse(time.RFC3339, s.UpdatedTime) + if err != nil { + t, err = time.Parse(gophercloud.RFC3339NoZ, s.UpdatedTime) + if err != nil { + return err + } + } + r.UpdatedTime = t + } + + return nil +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a RetrievedStack object and is called after a +// Get operation. +func (r GetResult) Extract() (*RetrievedStack, error) { + var s struct { + Stack *RetrievedStack `json:"stack"` + } + err := r.ExtractInto(&s) + return s.Stack, err +} + +// UpdateResult represents the result of a Update operation. +type UpdateResult struct { + gophercloud.ErrResult +} + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// PreviewedStack represents the result of a Preview operation. +type PreviewedStack struct { + Capabilities []interface{} `json:"capabilities"` + CreationTime time.Time `json:"-"` + Description string `json:"description"` + DisableRollback bool `json:"disable_rollback"` + ID string `json:"id"` + Links []gophercloud.Link `json:"links"` + Name string `json:"stack_name"` + NotificationTopics []interface{} `json:"notification_topics"` + Parameters map[string]string `json:"parameters"` + Resources []interface{} `json:"resources"` + TemplateDescription string `json:"template_description"` + Timeout int `json:"timeout_mins"` + UpdatedTime time.Time `json:"-"` +} + +func (r *PreviewedStack) UnmarshalJSON(b []byte) error { + type tmp PreviewedStack + var s struct { + tmp + CreationTime string `json:"creation_time"` + UpdatedTime string `json:"updated_time"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = PreviewedStack(s.tmp) + + if s.CreationTime != "" { + t, err := time.Parse(time.RFC3339, s.CreationTime) + if err != nil { + t, err = time.Parse(gophercloud.RFC3339NoZ, s.CreationTime) + if err != nil { + return err + } + } + r.CreationTime = t + } + + if s.UpdatedTime != "" { + t, err := time.Parse(time.RFC3339, s.UpdatedTime) + if err != nil { + t, err = time.Parse(gophercloud.RFC3339NoZ, s.UpdatedTime) + if err != nil { + return err + } + } + r.UpdatedTime = t + } + + return nil +} + +// PreviewResult represents the result of a Preview operation. +type PreviewResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a PreviewedStack object and is called after a +// Preview operation. +func (r PreviewResult) Extract() (*PreviewedStack, error) { + var s struct { + PreviewedStack *PreviewedStack `json:"stack"` + } + err := r.ExtractInto(&s) + return s.PreviewedStack, err +} + +// AbandonedStack represents the result of an Abandon operation. +type AbandonedStack struct { + Status string `json:"status"` + Name string `json:"name"` + Template map[string]interface{} `json:"template"` + Action string `json:"action"` + ID string `json:"id"` + Resources map[string]interface{} `json:"resources"` + Files map[string]string `json:"files"` + StackUserProjectID string `json:"stack_user_project_id"` + ProjectID string `json:"project_id"` + Environment map[string]interface{} `json:"environment"` +} + +// AbandonResult represents the result of an Abandon operation. +type AbandonResult struct { + gophercloud.Result +} + +// Extract returns a pointer to an AbandonedStack object and is called after an +// Abandon operation. +func (r AbandonResult) Extract() (*AbandonedStack, error) { + var s *AbandonedStack + err := r.ExtractInto(&s) + return s, err +} + +// String converts an AbandonResult to a string. This is useful to when passing +// the result of an Abandon operation to an AdoptOpts AdoptStackData field. +func (r AbandonResult) String() (string, error) { + out, err := json.Marshal(r) + return string(out), err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/template.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/template.go new file mode 100644 index 0000000000..4cf5aae41a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/template.go @@ -0,0 +1,141 @@ +package stacks + +import ( + "fmt" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud" +) + +// Template is a structure that represents OpenStack Heat templates +type Template struct { + TE +} + +// TemplateFormatVersions is a map containing allowed variations of the template format version +// Note that this contains the permitted variations of the _keys_ not the values. +var TemplateFormatVersions = map[string]bool{ + "HeatTemplateFormatVersion": true, + "heat_template_version": true, + "AWSTemplateFormatVersion": true, +} + +// Validate validates the contents of the Template +func (t *Template) Validate() error { + if t.Parsed == nil { + if err := t.Parse(); err != nil { + return err + } + } + var invalid string + for key := range t.Parsed { + if _, ok := TemplateFormatVersions[key]; ok { + return nil + } + invalid = key + } + return ErrInvalidTemplateFormatVersion{Version: invalid} +} + +// GetFileContents recursively parses a template to search for urls. These urls +// are assumed to point to other templates (known in OpenStack Heat as child +// templates). The contents of these urls are fetched and stored in the `Files` +// parameter of the template structure. This is the only way that a user can +// use child templates that are located in their filesystem; urls located on the +// web (e.g. on github or swift) can be fetched directly by Heat engine. +func (t *Template) getFileContents(te interface{}, ignoreIf igFunc, recurse bool) error { + // initialize template if empty + if t.Files == nil { + t.Files = make(map[string]string) + } + if t.fileMaps == nil { + t.fileMaps = make(map[string]string) + } + switch te.(type) { + // if te is a map + case map[string]interface{}, map[interface{}]interface{}: + teMap, err := toStringKeys(te) + if err != nil { + return err + } + for k, v := range teMap { + value, ok := v.(string) + if !ok { + // if the value is not a string, recursively parse that value + if err := t.getFileContents(v, ignoreIf, recurse); err != nil { + return err + } + } else if !ignoreIf(k, value) { + // at this point, the k, v pair has a reference to an external template. + // The assumption of heatclient is that value v is a reference + // to a file in the users environment + + // create a new child template + childTemplate := new(Template) + + // initialize child template + + // get the base location of the child template + baseURL, err := gophercloud.NormalizePathURL(t.baseURL, value) + if err != nil { + return err + } + childTemplate.baseURL = baseURL + childTemplate.client = t.client + + // fetch the contents of the child template + if err := childTemplate.Parse(); err != nil { + return err + } + + // process child template recursively if required. This is + // required if the child template itself contains references to + // other templates + if recurse { + if err := childTemplate.getFileContents(childTemplate.Parsed, ignoreIf, recurse); err != nil { + return err + } + } + // update parent template with current child templates' content. + // At this point, the child template has been parsed recursively. + t.fileMaps[value] = childTemplate.URL + t.Files[childTemplate.URL] = string(childTemplate.Bin) + + } + } + return nil + // if te is a slice, call the function on each element of the slice. + case []interface{}: + teSlice := te.([]interface{}) + for i := range teSlice { + if err := t.getFileContents(teSlice[i], ignoreIf, recurse); err != nil { + return err + } + } + // if te is anything else, return + case string, bool, float64, nil, int: + return nil + default: + return gophercloud.ErrUnexpectedType{Actual: fmt.Sprintf("%v", reflect.TypeOf(te))} + } + return nil +} + +// function to choose keys whose values are other template files +func ignoreIfTemplate(key string, value interface{}) bool { + // key must be either `get_file` or `type` for value to be a URL + if key != "get_file" && key != "type" { + return true + } + // value must be a string + valueString, ok := value.(string) + if !ok { + return true + } + // `.template` and `.yaml` are allowed suffixes for template URLs when referred to by `type` + if key == "type" && !(strings.HasSuffix(valueString, ".template") || strings.HasSuffix(valueString, ".yaml")) { + return true + } + return false +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/urls.go new file mode 100644 index 0000000000..b909caac86 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/urls.go @@ -0,0 +1,39 @@ +package stacks + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("stacks") +} + +func adoptURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func listURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func getURL(c *gophercloud.ServiceClient, name, id string) string { + return c.ServiceURL("stacks", name, id) +} + +func findURL(c *gophercloud.ServiceClient, identity string) string { + return c.ServiceURL("stacks", identity) +} + +func updateURL(c *gophercloud.ServiceClient, name, id string) string { + return getURL(c, name, id) +} + +func deleteURL(c *gophercloud.ServiceClient, name, id string) string { + return getURL(c, name, id) +} + +func previewURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("stacks", "preview") +} + +func abandonURL(c *gophercloud.ServiceClient, name, id string) string { + return c.ServiceURL("stacks", name, id, "abandon") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/utils.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/utils.go new file mode 100644 index 0000000000..8f8d4cc4c0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/utils.go @@ -0,0 +1,160 @@ +package stacks + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud" + yaml "gopkg.in/yaml.v2" +) + +// Client is an interface that expects a Get method similar to http.Get. This +// is needed for unit testing, since we can mock an http client. Thus, the +// client will usually be an http.Client EXCEPT in unit tests. +type Client interface { + Get(string) (*http.Response, error) +} + +// TE is a base structure for both Template and Environment +type TE struct { + // Bin stores the contents of the template or environment. + Bin []byte + // URL stores the URL of the template. This is allowed to be a 'file://' + // for local files. + URL string + // Parsed contains a parsed version of Bin. Since there are 2 different + // fields referring to the same value, you must be careful when accessing + // this filed. + Parsed map[string]interface{} + // Files contains a mapping between the urls in templates to their contents. + Files map[string]string + // fileMaps is a map used internally when determining Files. + fileMaps map[string]string + // baseURL represents the location of the template or environment file. + baseURL string + // client is an interface which allows TE to fetch contents from URLS + client Client +} + +// Fetch fetches the contents of a TE from its URL. Once a TE structure has a +// URL, call the fetch method to fetch the contents. +func (t *TE) Fetch() error { + // if the baseURL is not provided, use the current directors as the base URL + if t.baseURL == "" { + u, err := getBasePath() + if err != nil { + return err + } + t.baseURL = u + } + + // if the contents are already present, do nothing. + if t.Bin != nil { + return nil + } + + // get a fqdn from the URL using the baseURL of the TE. For local files, + // the URL's will have the `file` scheme. + u, err := gophercloud.NormalizePathURL(t.baseURL, t.URL) + if err != nil { + return err + } + t.URL = u + + // get an HTTP client if none present + if t.client == nil { + t.client = getHTTPClient() + } + + // use the client to fetch the contents of the TE + resp, err := t.client.Get(t.URL) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + t.Bin = body + return nil +} + +// get the basepath of the TE +func getBasePath() (string, error) { + basePath, err := filepath.Abs(".") + if err != nil { + return "", err + } + u, err := gophercloud.NormalizePathURL("", basePath) + if err != nil { + return "", err + } + return u, nil +} + +// get a an HTTP client to retrieve URL's. This client allows the use of `file` +// scheme since we may need to fetch files from users filesystem +func getHTTPClient() Client { + transport := &http.Transport{} + transport.RegisterProtocol("file", http.NewFileTransport(http.Dir("/"))) + return &http.Client{Transport: transport} +} + +// Parse will parse the contents and then validate. The contents MUST be either JSON or YAML. +func (t *TE) Parse() error { + if err := t.Fetch(); err != nil { + return err + } + if jerr := json.Unmarshal(t.Bin, &t.Parsed); jerr != nil { + if yerr := yaml.Unmarshal(t.Bin, &t.Parsed); yerr != nil { + return ErrInvalidDataFormat{} + } + } + return t.Validate() +} + +// Validate validates the contents of TE +func (t *TE) Validate() error { + return nil +} + +// igfunc is a parameter used by GetFileContents and GetRRFileContents to check +// for valid URL's. +type igFunc func(string, interface{}) bool + +// convert map[interface{}]interface{} to map[string]interface{} +func toStringKeys(m interface{}) (map[string]interface{}, error) { + switch m.(type) { + case map[string]interface{}, map[interface{}]interface{}: + typedMap := make(map[string]interface{}) + if _, ok := m.(map[interface{}]interface{}); ok { + for k, v := range m.(map[interface{}]interface{}) { + typedMap[k.(string)] = v + } + } else { + typedMap = m.(map[string]interface{}) + } + return typedMap, nil + default: + return nil, gophercloud.ErrUnexpectedType{Expected: "map[string]interface{}/map[interface{}]interface{}", Actual: fmt.Sprintf("%v", reflect.TypeOf(m))} + } +} + +// fix the reference to files by replacing relative URL's by absolute +// URL's +func (t *TE) fixFileRefs() { + tStr := string(t.Bin) + if t.fileMaps == nil { + return + } + for k, v := range t.fileMaps { + tStr = strings.Replace(tStr, k, v, -1) + } + t.Bin = []byte(tStr) +} diff --git a/vendor/github.com/gophercloud/utils/terraform/auth/config.go b/vendor/github.com/gophercloud/utils/terraform/auth/config.go index d4aa60d097..6f24639cc5 100644 --- a/vendor/github.com/gophercloud/utils/terraform/auth/config.go +++ b/vendor/github.com/gophercloud/utils/terraform/auth/config.go @@ -509,6 +509,26 @@ func (c *Config) ObjectStorageV1Client(region string) (*gophercloud.ServiceClien return client, nil } +func (c *Config) OrchestrationV1Client(region string) (*gophercloud.ServiceClient, error) { + if err := c.authenticate(); err != nil { + return nil, err + } + + client, err := openstack.NewOrchestrationV1(c.OsClient, gophercloud.EndpointOpts{ + Region: c.determineRegion(region), + Availability: clientconfig.GetEndpointType(c.EndpointType), + }) + + if err != nil { + return client, err + } + + // Check if an endpoint override was specified for the orchestration service. + client = c.determineEndpoint(client, "orchestration") + + return client, nil +} + func (c *Config) LoadBalancerV2Client(region string) (*gophercloud.ServiceClient, error) { if err := c.authenticate(); err != nil { return nil, err diff --git a/website/docs/r/orchestration_stack_v1.html.markdown b/website/docs/r/orchestration_stack_v1.html.markdown new file mode 100644 index 0000000000..eea7861137 --- /dev/null +++ b/website/docs/r/orchestration_stack_v1.html.markdown @@ -0,0 +1,95 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_orchestration_stack_v1" +sidebar_current: "docs-openstack-resource-orchestration-stack-v1" +description: |- + Manages a V1 stack resource within OpenStack. +--- + +# openstack\_orchestration\_stack_v1 + +Manages a V1 stack resource within OpenStack. + +## Example Usage + +```hcl +resource "openstack_orchestration_stack_v1" "stack_1" { + name = "stack_1" + parameters = { + length = 4 + } + template_opts = { + Bin = "heat_template_version: 2013-05-23\nparameters:\n length:\n type: number\nresources:\n test_res:\n type: OS::Heat::TestResource\n random:\n type: OS::Heat::RandomString\n properties:\n length: {get_param: length}\n" + } + environment_opts = { + Bin = "\n" + } + disable_rollback = true + timeout = 30 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional) The region in which to create the stack. If + omitted, the `region` argument of the provider is used. Changing this + creates a new stack. + +* `name` - (Required) A unique name for the stack. It must start with an + alphabetic character. Changing this updates the stack's name. + +* `template_opts` - (Required) Template key/value pairs to associate with the + stack which contains either the template file or url. + Allowed keys: Bin, URL, Files. Changing this updates the existing stack + Template Opts. + +* `environment_opts` - (Optional) Environment key/value pairs to associate with + the stack which contains details for the environment of the stack. + Allowed keys: Bin, URL, Files. Changing this updates the existing stack + Environment Opts. + +* `disable_rollback` - (Optional) Enables or disables deletion of all stack + resources when a stack creation fails. Default is true, meaning all + resources are not deleted when stack creation fails. + +* `parameters` - (Optional) User-defined key/value pairs as parameters to pass + to the template. Changing this updates the existing stack parameters. + +* `timeout` - (Optional) The timeout for stack action in minutes. + +* `tags` - (Optional) A list of tags to assosciate with the Stack + +## Attributes Reference + +The following attributes are exported: + +* `name` - See Argument Reference above. +* `disable_rollback` - See Argument Reference above. +* `timeout` - See Argument Reference above. +* `parameters` - See Argument Reference above. +* `tags` - See Argument Reference above. +* `capabilities` - List of stack capabilities for stack. +* `description` - The description of the stack resource. +* `notification_topics` - List of notification topics for stack. +* `status` - The status of the stack. +* `status_reason` - The reason for the current status of the stack. +* `template_description` - The description of the stack template. +* `outputs` - A list of stack outputs. +* `creation_time` - The date and time when the resource was created. The date + and time stamp format is ISO 8601: CCYY-MM-DDThh:mm:ss±hh:mm + For example, 2015-08-27T09:49:58-05:00. The ±hh:mm value, if included, + is the time zone as an offset from UTC. +* `updated_time` - The date and time when the resource was updated. The date + and time stamp format is ISO 8601: CCYY-MM-DDThh:mm:ss±hh:mm + For example, 2015-08-27T09:49:58-05:00. The ±hh:mm value, if included, + is the time zone as an offset from UTC. + +## Import + +stacks can be imported using the `id`, e.g. + +``` +$ terraform import openstack_orchestration_stack_v1.stack_1 ea257959-eeb1-4c10-8d33-26f0409a755d +``` diff --git a/website/openstack.erb b/website/openstack.erb index 9f6b058986..8d19d2a7c5 100644 --- a/website/openstack.erb +++ b/website/openstack.erb @@ -471,6 +471,15 @@ + > + Orchestration Resources + + + <% end %>