From 6ea0397e07455cadaed97f85b4cb1ca048f334af Mon Sep 17 00:00:00 2001 From: aznashwan Date: Fri, 26 Jun 2015 15:48:55 +0300 Subject: [PATCH] Made instances deployable on already existing services. --- builtin/providers/azure/provider_test.go | 5 +- .../azure/resource_azure_instance.go | 148 +++++++++++------- .../azure/resource_azure_instance_test.go | 134 ++++++++++++---- .../providers/azure/r/instance.html.markdown | 16 +- 4 files changed, 218 insertions(+), 85 deletions(-) diff --git a/builtin/providers/azure/provider_test.go b/builtin/providers/azure/provider_test.go index 8ab2ad0878..3d5ec342ce 100644 --- a/builtin/providers/azure/provider_test.go +++ b/builtin/providers/azure/provider_test.go @@ -11,7 +11,10 @@ import ( var testAccProviders map[string]terraform.ResourceProvider var testAccProvider *schema.Provider -const testAccSecurityGroupName = "terraform-security-group" +const ( + testAccSecurityGroupName = "terraform-security-group" + testAccHostedServiceName = "terraform-testing-service" +) // testAccStorageServiceName is used as the name for the Storage Service // created in all storage-related tests. diff --git a/builtin/providers/azure/resource_azure_instance.go b/builtin/providers/azure/resource_azure_instance.go index db5db9d38a..b273675f59 100644 --- a/builtin/providers/azure/resource_azure_instance.go +++ b/builtin/providers/azure/resource_azure_instance.go @@ -37,6 +37,13 @@ func resourceAzureInstance() *schema.Resource { ForceNew: true, }, + "hosted_service_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "description": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -197,36 +204,30 @@ func resourceAzureInstanceCreate(d *schema.ResourceData, meta interface{}) (err return err } - p := hostedservice.CreateHostedServiceParameters{ - ServiceName: name, - Label: base64.StdEncoding.EncodeToString([]byte(name)), - Description: fmt.Sprintf("Cloud Service created automatically for instance %s", name), - Location: d.Get("location").(string), - ReverseDNSFqdn: d.Get("reverse_dns").(string), - } + var hostedServiceName string + // check if hosted service name parameter was given: + if serviceName, ok := d.GetOk("hosted_service_name"); !ok { + // if not provided; just use the name of the instance to create a new one: + hostedServiceName = name + d.Set("hosted_service_name", hostedServiceName) - log.Printf("[DEBUG] Creating Cloud Service for instance: %s", name) - err = hostedServiceClient.CreateHostedService(p) - if err != nil { - return fmt.Errorf("Error creating Cloud Service for instance %s: %s", name, err) - } - - // Put in this defer here, so we are sure to cleanup already created parts - // when we exit with an error - defer func(mc management.Client) { - if err != nil { - req, err := hostedServiceClient.DeleteHostedService(name, true) - if err != nil { - log.Printf("[DEBUG] Error cleaning up Cloud Service of instance %s: %s", name, err) - } - - // Wait until the Cloud Service is deleted - if err := mc.WaitForOperation(req, nil); err != nil { - log.Printf( - "[DEBUG] Error waiting for Cloud Service of instance %s to be deleted: %s", name, err) - } + p := hostedservice.CreateHostedServiceParameters{ + ServiceName: hostedServiceName, + Label: base64.StdEncoding.EncodeToString([]byte(name)), + Description: fmt.Sprintf("Cloud Service created automatically for instance %s", name), + Location: d.Get("location").(string), + ReverseDNSFqdn: d.Get("reverse_dns").(string), } - }(mc) + + log.Printf("[DEBUG] Creating Cloud Service for instance: %s", name) + err = hostedServiceClient.CreateHostedService(p) + if err != nil { + return fmt.Errorf("Error creating Cloud Service for instance %s: %s", name, err) + } + } else { + // else; use the provided hosted service name: + hostedServiceName = serviceName.(string) + } // Create a new role for the instance role := vmutils.NewVMConfiguration(name, d.Get("size").(string)) @@ -312,7 +313,7 @@ func resourceAzureInstanceCreate(d *schema.ResourceData, meta interface{}) (err } log.Printf("[DEBUG] Creating the new instance...") - req, err := vmClient.CreateDeployment(role, name, options) + req, err := vmClient.CreateDeployment(role, hostedServiceName, options) if err != nil { return fmt.Errorf("Error creating instance %s: %s", name, err) } @@ -333,28 +334,41 @@ func resourceAzureInstanceRead(d *schema.ResourceData, meta interface{}) error { hostedServiceClient := azureClient.hostedServiceClient vmClient := azureClient.vmClient - log.Printf("[DEBUG] Retrieving Cloud Service for instance: %s", d.Id()) - cs, err := hostedServiceClient.GetHostedService(d.Id()) + name := d.Get("name").(string) + + // check if the instance belongs to an independent hosted service + // or it had one created for it. + var hostedServiceName string + if serviceName, ok := d.GetOk("hosted_service_name"); ok { + // if independent; use that hosted service name: + hostedServiceName = serviceName.(string) + } else { + // else; suppose it's the instance's name: + hostedServiceName = name + } + + log.Printf("[DEBUG] Retrieving Cloud Service for instance: %s", name) + cs, err := hostedServiceClient.GetHostedService(hostedServiceName) if err != nil { - return fmt.Errorf("Error retrieving Cloud Service of instance %s: %s", d.Id(), err) + return fmt.Errorf("Error retrieving Cloud Service of instance %s (%q): %s", name, hostedServiceName, err) } d.Set("reverse_dns", cs.ReverseDNSFqdn) d.Set("location", cs.Location) - log.Printf("[DEBUG] Retrieving instance: %s", d.Id()) - dpmt, err := vmClient.GetDeployment(d.Id(), d.Id()) + log.Printf("[DEBUG] Retrieving instance: %s", name) + dpmt, err := vmClient.GetDeployment(hostedServiceName, name) if err != nil { if management.IsResourceNotFoundError(err) { d.SetId("") return nil } - return fmt.Errorf("Error retrieving instance %s: %s", d.Id(), err) + return fmt.Errorf("Error retrieving instance %s: %s", name, err) } if len(dpmt.RoleList) != 1 { return fmt.Errorf( - "Instance %s has an unexpected number of roles: %d", d.Id(), len(dpmt.RoleList)) + "Instance %s has an unexpected number of roles: %d", name, len(dpmt.RoleList)) } d.Set("size", dpmt.RoleList[0].RoleSize) @@ -362,7 +376,7 @@ func resourceAzureInstanceRead(d *schema.ResourceData, meta interface{}) error { if len(dpmt.RoleInstanceList) != 1 { return fmt.Errorf( "Instance %s has an unexpected number of role instances: %d", - d.Id(), len(dpmt.RoleInstanceList)) + name, len(dpmt.RoleInstanceList)) } d.Set("ip_address", dpmt.RoleInstanceList[0].IPAddress) @@ -400,7 +414,7 @@ func resourceAzureInstanceRead(d *schema.ResourceData, meta interface{}) error { default: return fmt.Errorf( "Instance %s has an unexpected number of associated subnets %d", - d.Id(), len(dpmt.RoleInstanceList)) + name, len(dpmt.RoleInstanceList)) } // Update the security group @@ -434,10 +448,13 @@ func resourceAzureInstanceUpdate(d *schema.ResourceData, meta interface{}) error return nil } + name := d.Get("name").(string) + hostedServiceName := d.Get("hosted_service_name").(string) + // Get the current role - role, err := vmClient.GetRole(d.Id(), d.Id(), d.Id()) + role, err := vmClient.GetRole(hostedServiceName, name, name) if err != nil { - return fmt.Errorf("Error retrieving role of instance %s: %s", d.Id(), err) + return fmt.Errorf("Error retrieving role of instance %s: %s", name, err) } // Verify if we have all required parameters @@ -473,7 +490,7 @@ func resourceAzureInstanceUpdate(d *schema.ResourceData, meta interface{}) error ) if err != nil { return fmt.Errorf( - "Error adding endpoint %s for instance %s: %s", m["name"].(string), d.Id(), err) + "Error adding endpoint %s for instance %s: %s", m["name"].(string), name, err) } } } @@ -484,19 +501,19 @@ func resourceAzureInstanceUpdate(d *schema.ResourceData, meta interface{}) error err := vmutils.ConfigureWithSecurityGroup(role, sg) if err != nil { return fmt.Errorf( - "Error associating security group %s with instance %s: %s", sg, d.Id(), err) + "Error associating security group %s with instance %s: %s", sg, name, err) } } // Update the adjusted role - req, err := vmClient.UpdateRole(d.Id(), d.Id(), d.Id(), *role) + req, err := vmClient.UpdateRole(hostedServiceName, name, name, *role) if err != nil { - return fmt.Errorf("Error updating role of instance %s: %s", d.Id(), err) + return fmt.Errorf("Error updating role of instance %s: %s", name, err) } if err := mc.WaitForOperation(req, nil); err != nil { return fmt.Errorf( - "Error waiting for role of instance %s to be updated: %s", d.Id(), err) + "Error waiting for role of instance %s to be updated: %s", name, err) } return resourceAzureInstanceRead(d, meta) @@ -505,21 +522,40 @@ func resourceAzureInstanceUpdate(d *schema.ResourceData, meta interface{}) error func resourceAzureInstanceDelete(d *schema.ResourceData, meta interface{}) error { azureClient := meta.(*Client) mc := azureClient.mgmtClient + vmClient := azureClient.vmClient hostedServiceClient := azureClient.hostedServiceClient - log.Printf("[DEBUG] Deleting instance: %s", d.Id()) - req, err := hostedServiceClient.DeleteHostedService(d.Id(), true) - if err != nil { - return fmt.Errorf("Error deleting instance %s: %s", d.Id(), err) - } + name := d.Get("name").(string) + hostedServiceName := d.Get("hosted_service_name").(string) - // Wait until the instance is deleted - if err := mc.WaitForOperation(req, nil); err != nil { - return fmt.Errorf( - "Error waiting for instance %s to be deleted: %s", d.Id(), err) - } + log.Printf("[DEBUG] Deleting instance: %s", name) - d.SetId("") + // check if the instance had a hosted service created especially for it: + if name == hostedServiceName { + // if so; we must delete the associated hosted service as well: + req, err := hostedServiceClient.DeleteHostedService(name, true) + if err != nil { + return fmt.Errorf("Error deleting instance and hosted service %s: %s", name, err) + } + + // Wait until the hosted service and the instance it contains is deleted: + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for instance %s to be deleted: %s", name, err) + } + } else { + // else; just delete the instance: + reqID, err := vmClient.DeleteDeployment(hostedServiceName, name) + if err != nil { + return fmt.Errorf("Error deleting instance %s off hosted service %s: %s", name, hostedServiceName, err) + } + + // and wait for the deletion: + if err := mc.WaitForOperation(reqID, nil); err != nil { + return fmt.Errorf("Error waiting for intance %s to be deleted off the hosted service %s: %s", + name, hostedServiceName, err) + } + } return nil } diff --git a/builtin/providers/azure/resource_azure_instance_test.go b/builtin/providers/azure/resource_azure_instance_test.go index 755bed308b..679aa015ba 100644 --- a/builtin/providers/azure/resource_azure_instance_test.go +++ b/builtin/providers/azure/resource_azure_instance_test.go @@ -16,16 +16,46 @@ func TestAccAzureInstance_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: testAccCheckAzureInstanceDestroy, + CheckDestroy: testAccCheckAzureInstanceDestroyed(""), Steps: []resource.TestStep{ resource.TestStep{ Config: testAccAzureInstance_basic, Check: resource.ComposeTestCheckFunc( testAccCheckAzureInstanceExists( - "azure_instance.foo", &dpmt), + "azure_instance.foo", "", &dpmt), testAccCheckAzureInstanceBasicAttributes(&dpmt), resource.TestCheckResourceAttr( "azure_instance.foo", "name", "terraform-test"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "hosted_service_name", "terraform-test"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "location", "West US"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "endpoint.2462817782.public_port", "22"), + ), + }, + }, + }) +} + +func TestAccAzureInstance_separateHostedService(t *testing.T) { + var dpmt virtualmachine.DeploymentResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureInstanceDestroyed(testAccHostedServiceName), + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureInstance_seperateHostedService, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureInstanceExists( + "azure_instance.foo", testAccHostedServiceName, &dpmt), + testAccCheckAzureInstanceBasicAttributes(&dpmt), + resource.TestCheckResourceAttr( + "azure_instance.foo", "name", "terraform-test"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "hosted_service_name", "terraform-testing-service"), resource.TestCheckResourceAttr( "azure_instance.foo", "location", "West US"), resource.TestCheckResourceAttr( @@ -42,16 +72,18 @@ func TestAccAzureInstance_advanced(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: testAccCheckAzureInstanceDestroy, + CheckDestroy: testAccCheckAzureInstanceDestroyed(""), Steps: []resource.TestStep{ resource.TestStep{ Config: testAccAzureInstance_advanced, Check: resource.ComposeTestCheckFunc( testAccCheckAzureInstanceExists( - "azure_instance.foo", &dpmt), + "azure_instance.foo", "", &dpmt), testAccCheckAzureInstanceAdvancedAttributes(&dpmt), resource.TestCheckResourceAttr( "azure_instance.foo", "name", "terraform-test1"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "hosted_service_name", "terraform-test1"), resource.TestCheckResourceAttr( "azure_instance.foo", "size", "Basic_A1"), resource.TestCheckResourceAttr( @@ -74,16 +106,18 @@ func TestAccAzureInstance_update(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: testAccCheckAzureInstanceDestroy, + CheckDestroy: testAccCheckAzureInstanceDestroyed(""), Steps: []resource.TestStep{ resource.TestStep{ Config: testAccAzureInstance_advanced, Check: resource.ComposeTestCheckFunc( testAccCheckAzureInstanceExists( - "azure_instance.foo", &dpmt), + "azure_instance.foo", "", &dpmt), testAccCheckAzureInstanceAdvancedAttributes(&dpmt), resource.TestCheckResourceAttr( "azure_instance.foo", "name", "terraform-test1"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "hosted_service_name", "terraform-test1"), resource.TestCheckResourceAttr( "azure_instance.foo", "size", "Basic_A1"), resource.TestCheckResourceAttr( @@ -101,7 +135,7 @@ func TestAccAzureInstance_update(t *testing.T) { Config: testAccAzureInstance_update, Check: resource.ComposeTestCheckFunc( testAccCheckAzureInstanceExists( - "azure_instance.foo", &dpmt), + "azure_instance.foo", "", &dpmt), testAccCheckAzureInstanceUpdatedAttributes(&dpmt), resource.TestCheckResourceAttr( "azure_instance.foo", "size", "Basic_A2"), @@ -119,6 +153,7 @@ func TestAccAzureInstance_update(t *testing.T) { func testAccCheckAzureInstanceExists( n string, + hostedServiceName string, dpmt *virtualmachine.DeploymentResponse) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -130,8 +165,17 @@ func testAccCheckAzureInstanceExists( return fmt.Errorf("No instance ID is set") } + // if not hosted service was provided; it means that we expect it + // to be identical with the name of the instance; which is in the ID. + var serviceName string + if hostedServiceName == "" { + serviceName = rs.Primary.ID + } else { + serviceName = hostedServiceName + } + vmClient := testAccProvider.Meta().(*Client).vmClient - vm, err := vmClient.GetDeployment(rs.Primary.ID, rs.Primary.ID) + vm, err := vmClient.GetDeployment(serviceName, rs.Primary.ID) if err != nil { return err } @@ -281,29 +325,40 @@ func testAccCheckAzureInstanceUpdatedAttributes( } } -func testAccCheckAzureInstanceDestroy(s *terraform.State) error { - hostedServiceClient := testAccProvider.Meta().(*Client).hostedServiceClient +func testAccCheckAzureInstanceDestroyed(hostedServiceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + hostedServiceClient := testAccProvider.Meta().(*Client).hostedServiceClient - for _, rs := range s.RootModule().Resources { - if rs.Type != "azure_instance" { - continue + for _, rs := range s.RootModule().Resources { + if rs.Type != "azure_instance" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + // if not hosted service was provided; it means that we expect it + // to be identical with the name of the instance; which is in the ID. + var serviceName string + if hostedServiceName == "" { + serviceName = rs.Primary.ID + } else { + serviceName = hostedServiceName + } + + _, err := hostedServiceClient.GetHostedService(serviceName) + if err == nil { + return fmt.Errorf("Instance %s still exists", rs.Primary.ID) + } + + if !management.IsResourceNotFoundError(err) { + return err + } } - if rs.Primary.ID == "" { - return fmt.Errorf("No instance ID is set") - } - - _, err := hostedServiceClient.GetHostedService(rs.Primary.ID) - if err == nil { - return fmt.Errorf("Instance %s still exists", rs.Primary.ID) - } - - if !management.IsResourceNotFoundError(err) { - return err - } + return nil } - - return nil } var testAccAzureInstance_basic = fmt.Sprintf(` @@ -324,6 +379,31 @@ resource "azure_instance" "foo" { } }`, testAccStorageServiceName) +var testAccAzureInstance_seperateHostedService = fmt.Sprintf(` +resource "azure_hosted_service" "foo" { + name = "%s" + location = "West US" + ephemeral_contents = true +} + +resource "azure_instance" "foo" { + name = "terraform-test" + hosted_service_name = "${azure_hosted_service.foo.name}" + image = "Ubuntu Server 14.04 LTS" + size = "Basic_A1" + storage_service_name = "%s" + location = "West US" + username = "terraform" + password = "Pass!admin123" + + endpoint { + name = "SSH" + protocol = "tcp" + public_port = 22 + private_port = 22 + } +}`, testAccHostedServiceName, testAccStorageServiceName) + var testAccAzureInstance_advanced = fmt.Sprintf(` resource "azure_virtual_network" "foo" { name = "terraform-vnet" diff --git a/website/source/docs/providers/azure/r/instance.html.markdown b/website/source/docs/providers/azure/r/instance.html.markdown index fb23df4ce0..6b94287626 100644 --- a/website/source/docs/providers/azure/r/instance.html.markdown +++ b/website/source/docs/providers/azure/r/instance.html.markdown @@ -8,14 +8,23 @@ description: |- # azure\_instance -Creates a hosted service, role and deployment and then creates a virtual +Creates a hosted service, role and deployment and then creates a virtual machine in the deployment based on the specified configuration. ## Example Usage ``` +resource "azure_hosted_service" "terraform-service" { + name = "terraform-service" + location = "North Europe" + ephemeral_contents = false + description = "Hosted service created by Terraform." + label = "tf-hs-01" +} + resource "azure_instance" "web" { name = "terraform-test" + hosted_service_name = "${azure_hosted_service.terraform-service.name}" image = "Ubuntu Server 14.04 LTS" size = "Basic_A1" storage_service_name = "yourstorage" @@ -39,6 +48,11 @@ The following arguments are supported: * `name` - (Required) The name of the instance. Changing this forces a new resource to be created. +* `hosted_service_name` - (Optional) The name of the hosted service the + instance should be deployed under. If not provided; it will default to the + value of `name`. Changes to this parameter forces the creation of a new + resource. + * `description` - (Optional) The description for the associated hosted service. Changing this forces a new resource to be created (defaults to the instance name).