From 1f07e3fe22e5f25a56159062c03ea5e548458188 Mon Sep 17 00:00:00 2001 From: njucz Date: Thu, 23 Apr 2020 11:47:07 +0800 Subject: [PATCH] new resource `azurerm_spring_cloud_app` --- .../services/appplatform/client/client.go | 5 + .../appplatform/parse/spring_cloud_app.go | 38 +++++ .../parse/spring_cloud_app_test.go | 88 +++++++++++ .../services/appplatform/registration.go | 1 + .../resource_arm_spring_cloud_app.go | 139 +++++++++++++++++ .../resource_arm_spring_cloud_app_test.go | 145 ++++++++++++++++++ .../appplatform/validate/spring_cloud_app.go | 25 +++ .../validate/spring_cloud_app_test.go | 61 ++++++++ website/azurerm.erb | 6 +- website/docs/r/spring_cloud_app.html.markdown | 68 ++++++++ 10 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 azurerm/internal/services/appplatform/parse/spring_cloud_app.go create mode 100644 azurerm/internal/services/appplatform/parse/spring_cloud_app_test.go create mode 100644 azurerm/internal/services/appplatform/resource_arm_spring_cloud_app.go create mode 100644 azurerm/internal/services/appplatform/tests/resource_arm_spring_cloud_app_test.go create mode 100644 azurerm/internal/services/appplatform/validate/spring_cloud_app.go create mode 100644 azurerm/internal/services/appplatform/validate/spring_cloud_app_test.go create mode 100644 website/docs/r/spring_cloud_app.html.markdown diff --git a/azurerm/internal/services/appplatform/client/client.go b/azurerm/internal/services/appplatform/client/client.go index db4af5abedd2..a0a6953b8c0a 100644 --- a/azurerm/internal/services/appplatform/client/client.go +++ b/azurerm/internal/services/appplatform/client/client.go @@ -7,13 +7,18 @@ import ( type Client struct { ServicesClient *appplatform.ServicesClient + AppsClient *appplatform.AppsClient } func NewClient(o *common.ClientOptions) *Client { + appsClient := appplatform.NewAppsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&appsClient.Client, o.ResourceManagerAuthorizer) + servicesClient := appplatform.NewServicesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&servicesClient.Client, o.ResourceManagerAuthorizer) return &Client{ + AppsClient: &appsClient, ServicesClient: &servicesClient, } } diff --git a/azurerm/internal/services/appplatform/parse/spring_cloud_app.go b/azurerm/internal/services/appplatform/parse/spring_cloud_app.go new file mode 100644 index 000000000000..be2038282915 --- /dev/null +++ b/azurerm/internal/services/appplatform/parse/spring_cloud_app.go @@ -0,0 +1,38 @@ +package parse + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type SpringCloudAppId struct { + ResourceGroup string + ServiceName string + Name string +} + +func SpringCloudAppID(input string) (*SpringCloudAppId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("parsing Spring Cloud App ID %q: %+v", input, err) + } + + app := SpringCloudAppId{ + ResourceGroup: id.ResourceGroup, + } + + if app.ServiceName, err = id.PopSegment("Spring"); err != nil { + return nil, err + } + + if app.Name, err = id.PopSegment("apps"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &app, nil +} diff --git a/azurerm/internal/services/appplatform/parse/spring_cloud_app_test.go b/azurerm/internal/services/appplatform/parse/spring_cloud_app_test.go new file mode 100644 index 000000000000..3f783629fad8 --- /dev/null +++ b/azurerm/internal/services/appplatform/parse/spring_cloud_app_test.go @@ -0,0 +1,88 @@ +package parse + +import ( + "testing" +) + +func TestSpringCloudAppID(t *testing.T) { + testData := []struct { + Name string + Input string + Expected *SpringCloudAppId + }{ + { + Name: "Empty", + Input: "", + Expected: nil, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Expected: nil, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Expected: nil, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Expected: nil, + }, + { + Name: "No Spring Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.AppPlatform/Spring/", + Expected: nil, + }, + { + Name: "Missing Apps Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.AppPlatform/Spring/spring1/", + Expected: nil, + }, + { + Name: "Missing Apps Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.AppPlatform/Spring/spring1/apps/", + Expected: nil, + }, + { + Name: "Spring Cloud App ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.AppPlatform/Spring/spring1/apps/app1", + Expected: &SpringCloudAppId{ + ResourceGroup: "resGroup1", + ServiceName: "spring1", + Name: "app1", + }, + }, + { + Name: "Wrong Casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.AppPlatform/Spring/spring1/Apps/app1", + Expected: nil, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := SpringCloudAppID(v.Input) + if err != nil { + if v.Expected == nil { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expected.ResourceGroup, actual.ResourceGroup) + } + + if actual.ServiceName != v.Expected.ServiceName { + t.Fatalf("Expected %q but got %q for ServiceName", v.Expected.ServiceName, actual.ServiceName) + } + + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + } +} diff --git a/azurerm/internal/services/appplatform/registration.go b/azurerm/internal/services/appplatform/registration.go index 0a6b19ad9b3b..f048e38b406c 100644 --- a/azurerm/internal/services/appplatform/registration.go +++ b/azurerm/internal/services/appplatform/registration.go @@ -28,6 +28,7 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { // SupportedResources returns the supported Resources supported by this Service func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ + "azurerm_spring_cloud_app": resourceArmSpringCloudApp(), "azurerm_spring_cloud_service": resourceArmSpringCloudService(), } } diff --git a/azurerm/internal/services/appplatform/resource_arm_spring_cloud_app.go b/azurerm/internal/services/appplatform/resource_arm_spring_cloud_app.go new file mode 100644 index 000000000000..492db346536f --- /dev/null +++ b/azurerm/internal/services/appplatform/resource_arm_spring_cloud_app.go @@ -0,0 +1,139 @@ +package appplatform + +import ( + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/preview/appplatform/mgmt/2019-05-01-preview/appplatform" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/appplatform/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/appplatform/validate" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmSpringCloudApp() *schema.Resource { + return &schema.Resource{ + Create: resourceArmSpringCloudAppCreate, + Read: resourceArmSpringCloudAppRead, + Update: nil, + Delete: resourceArmSpringCloudAppDelete, + + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.SpringCloudAppID(id) + return err + }), + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.SpringCloudAppName, + }, + + "resource_group_name": azure.SchemaResourceGroupName(), + + "service_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.SpringCloudServiceName, + }, + }, + } +} + +func resourceArmSpringCloudAppCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).AppPlatform.AppsClient + ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + serviceName := d.Get("service_name").(string) + + existing, err := client.Get(ctx, resourceGroup, serviceName, name, "") + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("checking for present of existing Spring Cloud App %q (Spring Cloud Service %q / Resource Group %q): %+v", name, serviceName, resourceGroup, err) + } + } + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_spring_cloud_app", *existing.ID) + } + + future, err := client.CreateOrUpdate(ctx, resourceGroup, serviceName, name, appplatform.AppResource{}) + if err != nil { + return fmt.Errorf("creating Spring Cloud App %q (Spring Cloud Service %q / Resource Group %q): %+v", name, serviceName, resourceGroup, err) + } + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for creation of Spring Cloud App %q (Spring Cloud Service %q / Resource Group %q): %+v", name, serviceName, resourceGroup, err) + } + + resp, err := client.Get(ctx, resourceGroup, serviceName, name, "") + if err != nil { + return fmt.Errorf("retrieving Spring Cloud App %q (Spring Cloud Service %q / Resource Group %q): %+v", name, serviceName, resourceGroup, err) + } + if resp.ID == nil || *resp.ID == "" { + return fmt.Errorf("read Spring Cloud App %q (Spring Cloud Service %q / Resource Group %q) ID", name, serviceName, resourceGroup) + } + d.SetId(*resp.ID) + + return resourceArmSpringCloudAppRead(d, meta) +} + +func resourceArmSpringCloudAppRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).AppPlatform.AppsClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.SpringCloudAppID(d.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.ServiceName, id.Name, "") + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] Spring Cloud App %q does not exist - removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("reading Spring Cloud App %q (Spring Cloud Service %q / Resource Group %q): %+v", id.Name, id.ServiceName, id.ResourceGroup, err) + } + + d.Set("name", resp.Name) + d.Set("resource_group_name", id.ResourceGroup) + d.Set("service_name", id.ServiceName) + + return nil +} + +func resourceArmSpringCloudAppDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).AppPlatform.AppsClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.SpringCloudAppID(d.Id()) + if err != nil { + return err + } + + if _, err := client.Delete(ctx, id.ResourceGroup, id.ServiceName, id.Name); err != nil { + return fmt.Errorf("deleting Spring Cloud App %q (Spring Cloud Service %q / Resource Group %q): %+v", id.Name, id.ServiceName, id.ResourceGroup, err) + } + + return nil +} diff --git a/azurerm/internal/services/appplatform/tests/resource_arm_spring_cloud_app_test.go b/azurerm/internal/services/appplatform/tests/resource_arm_spring_cloud_app_test.go new file mode 100644 index 000000000000..65d7b28b571f --- /dev/null +++ b/azurerm/internal/services/appplatform/tests/resource_arm_spring_cloud_app_test.go @@ -0,0 +1,145 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMSpringCloudApp_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_spring_cloud_app", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMSpringCloudAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMSpringCloudApp_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSpringCloudAppExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMSpringCloudApp_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_spring_cloud_app", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMSpringCloudAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMSpringCloudApp_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSpringCloudAppExists(data.ResourceName), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMSpringCloudApp_requiresImport), + }, + }) +} + +func testCheckAzureRMSpringCloudAppExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Spring Cloud App not found: %s", resourceName) + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + serviceName := rs.Primary.Attributes["service_name"] + + client := acceptance.AzureProvider.Meta().(*clients.Client).AppPlatform.AppsClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + if resp, err := client.Get(ctx, resourceGroup, serviceName, name, ""); err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("bad: Spring Cloud App %q (Spring Cloud Name %q / Resource Group %q) does not exist", name, serviceName, resourceGroup) + } + return fmt.Errorf("bad: Get on AppPlatform.AppsClient: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMSpringCloudAppDestroy(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).AppPlatform.AppsClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_spring_cloud_app" { + continue + } + + name := rs.Primary.Attributes["name"] + resGroup := rs.Primary.Attributes["resource_group_name"] + serviceName := rs.Primary.Attributes["service_name"] + + if resp, err := client.Get(ctx, resGroup, serviceName, name, ""); err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("bad: Get on AppPlatform.AppsClient: %+v", err) + } + } + + return nil + } + + return nil +} + +func testAccAzureRMSpringCloudApp_basic(data acceptance.TestData) string { + template := testAccAzureRMSpringCloudApp_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_spring_cloud_app" "test" { + name = "acctest-sca-%d" + resource_group_name = azurerm_spring_cloud_service.test.resource_group_name + service_name = azurerm_spring_cloud_service.test.name +} +`, template, data.RandomInteger) +} + +func testAccAzureRMSpringCloudApp_requiresImport(data acceptance.TestData) string { + template := testAccAzureRMSpringCloudApp_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_spring_cloud_app" "import" { + name = azurerm_spring_cloud_app.test.name + resource_group_name = azurerm_spring_cloud_app.test.resource_group_name + service_name = azurerm_spring_cloud_app.test.service_name +} +`, template) +} + +func testAccAzureRMSpringCloudApp_template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-spring-%d" + location = "%s" +} + +resource "azurerm_spring_cloud_service" "test" { + name = "acctest-sc-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} diff --git a/azurerm/internal/services/appplatform/validate/spring_cloud_app.go b/azurerm/internal/services/appplatform/validate/spring_cloud_app.go new file mode 100644 index 000000000000..83891b2433ac --- /dev/null +++ b/azurerm/internal/services/appplatform/validate/spring_cloud_app.go @@ -0,0 +1,25 @@ +package validate + +import ( + "fmt" + "regexp" +) + +func SpringCloudAppName(i interface{}, k string) (_ []string, errors []error) { + v, ok := i.(string) + if !ok { + return nil, append(errors, fmt.Errorf("expected type of %s to be string", k)) + } + + // The name attribute rules are : + // 1. can contain only lowercase letters, numbers and hyphens. + // 2. The first character must be a letter. + // 3. The last character must be a letter or number + // 3. The value must be between 4 and 32 characters long + + if !regexp.MustCompile(`^([a-z])([a-z\d-]{2,30})([a-z\d])$`).MatchString(v) { + errors = append(errors, fmt.Errorf("%s must begin with a letter, end with a letter or number, contain only lowercase letters, numbers and hyphens. The value must be between 4 and 32 characters long", k)) + } + + return nil, errors +} diff --git a/azurerm/internal/services/appplatform/validate/spring_cloud_app_test.go b/azurerm/internal/services/appplatform/validate/spring_cloud_app_test.go new file mode 100644 index 000000000000..d4ccf12bdbdd --- /dev/null +++ b/azurerm/internal/services/appplatform/validate/spring_cloud_app_test.go @@ -0,0 +1,61 @@ +package validate + +import "testing" + +func TestSpringCloudAppName(t *testing.T) { + testData := []struct { + input string + expected bool + }{ + { + // empty + input: "", + expected: false, + }, + { + // basic example + input: "ab-c", + expected: true, + }, + { + // can't start with a number + input: "1abc", + expected: false, + }, + { + // can't contain underscore + input: "ab_c", + expected: false, + }, + { + // can't end with hyphen + input: "abc-", + expected: false, + }, + { + // can not short than 4 characters + input: "abc", + expected: false, + }, + { + // 32 chars + input: "abcdefghijklmnopqrstuvwxyzabcdef", + expected: true, + }, + { + // 33 chars + input: "abcdefghijklmnopqrstuvwxyzabcdefg", + expected: false, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q..", v.input) + + _, errors := SpringCloudAppName(v.input, "name") + actual := len(errors) == 0 + if v.expected != actual { + t.Fatalf("Expected %t but got %t", v.expected, actual) + } + } +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 1618986cac7a..14916db0fc2d 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -800,8 +800,12 @@
  • - App Platform + Spring Cloud