diff --git a/azurerm/config.go b/azurerm/config.go index 9d924c2725e0..fba59f340e1a 100644 --- a/azurerm/config.go +++ b/azurerm/config.go @@ -131,6 +131,7 @@ type ArmClient struct { apiManagementGroupClient apimanagement.GroupClient apiManagementGroupUsersClient apimanagement.GroupUserClient apiManagementLoggerClient apimanagement.LoggerClient + apiManagementPolicyClient apimanagement.PolicyClient apiManagementProductsClient apimanagement.ProductClient apiManagementProductApisClient apimanagement.ProductAPIClient apiManagementProductGroupsClient apimanagement.ProductGroupClient @@ -518,6 +519,10 @@ func (c *ArmClient) registerApiManagementServiceClients(endpoint, subscriptionId c.configureClient(&serviceClient.Client, auth) c.apiManagementServiceClient = serviceClient + policyClient := apimanagement.NewPolicyClientWithBaseURI(endpoint, subscriptionId) + c.configureClient(&policyClient.Client, auth) + c.apiManagementPolicyClient = policyClient + productsClient := apimanagement.NewProductClientWithBaseURI(endpoint, subscriptionId) c.configureClient(&productsClient.Client, auth) c.apiManagementProductsClient = productsClient diff --git a/azurerm/helpers/suppress/xml.go b/azurerm/helpers/suppress/xml.go new file mode 100644 index 000000000000..6f53ad2b769f --- /dev/null +++ b/azurerm/helpers/suppress/xml.go @@ -0,0 +1,47 @@ +package suppress + +import ( + "encoding/xml" + "io" + "reflect" + "strings" + + "github.com/hashicorp/terraform/helper/schema" +) + +func SuppressXmlDiff(_, old, new string, _ *schema.ResourceData) bool { + oldTokens, err := expandXmlTokensFromString(old) + if err != nil { + return false + } + + newTokens, err := expandXmlTokensFromString(new) + if err != nil { + return false + } + + return reflect.DeepEqual(oldTokens, newTokens) +} + +// This function will extract all XML tokens from a string, but ignoring all white-space tokens +func expandXmlTokensFromString(input string) ([]xml.Token, error) { + decoder := xml.NewDecoder(strings.NewReader(input)) + tokens := make([]xml.Token, 0) + for { + token, err := decoder.Token() + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + if chars, ok := token.(xml.CharData); ok { + text := string(chars) + if strings.TrimSpace(text) == "" { + continue + } + } + tokens = append(tokens, xml.CopyToken(token)) + } + return tokens, nil +} diff --git a/azurerm/helpers/suppress/xml_test.go b/azurerm/helpers/suppress/xml_test.go new file mode 100644 index 000000000000..3a10a284238e --- /dev/null +++ b/azurerm/helpers/suppress/xml_test.go @@ -0,0 +1,81 @@ +package suppress + +import "testing" + +func TestSuppressXmlDiff(t *testing.T) { + cases := []struct { + Name string + XmlA string + XmlB string + Suppress bool + }{ + { + Name: "empty", + XmlA: "", + XmlB: "", + Suppress: true, + }, + { + Name: "neither are xml", + XmlA: "this is not an xml", + XmlB: "neither is this", + Suppress: false, + }, + { + Name: "identical texts", + XmlA: "this is not an xml", + XmlB: "this is not an xml", + Suppress: true, + }, + { + Name: "xml vs text", + XmlA: "", + XmlB: "this is not an xml", + Suppress: false, + }, + { + Name: "text vs xml", + XmlA: "this is not an xml", + XmlB: "", + Suppress: false, + }, + { + Name: "identical xml", + XmlA: "", + XmlB: "", + Suppress: true, + }, + { + Name: "xml with different line endings", + XmlA: "\n\n\n", + XmlB: "\r\n\r\n\r\n", + Suppress: true, + }, + { + Name: "xml with different indentations", + XmlA: "\n \n \n", + XmlB: "\r\n\t\r\n\t\r\n", + Suppress: true, + }, + { + Name: "xml with different quotation marks", + XmlA: "", + XmlB: "\r\n\t\r\n\t\r\n", + Suppress: true, + }, + { + Name: "xml with different spaces", + XmlA: "", + XmlB: "\r\n\t\r\n\t\r\n", + Suppress: true, + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + if SuppressXmlDiff("test", tc.XmlA, tc.XmlB, nil) != tc.Suppress { + t.Fatalf("Expected SuppressXmlDiff to return %t for '%q' == '%q'", tc.Suppress, tc.XmlA, tc.XmlB) + } + }) + } +} diff --git a/azurerm/provider.go b/azurerm/provider.go index 5ebae851ffe8..e146bcb361aa 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -173,6 +173,7 @@ func Provider() terraform.ResourceProvider { "azurerm_api_management_group": resourceArmApiManagementGroup(), "azurerm_api_management_group_user": resourceArmApiManagementGroupUser(), "azurerm_api_management_logger": resourceArmApiManagementLogger(), + "azurerm_api_management_policy": resourceArmApiManagementPolicy(), "azurerm_api_management_product": resourceArmApiManagementProduct(), "azurerm_api_management_product_api": resourceArmApiManagementProductApi(), "azurerm_api_management_product_group": resourceArmApiManagementProductGroup(), diff --git a/azurerm/resource_arm_api_management_policy.go b/azurerm/resource_arm_api_management_policy.go new file mode 100644 index 000000000000..d393be9321f6 --- /dev/null +++ b/azurerm/resource_arm_api_management_policy.go @@ -0,0 +1,154 @@ +package azurerm + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/services/apimanagement/mgmt/2018-01-01/apimanagement" + "github.com/hashicorp/terraform/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmApiManagementPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceArmApiManagementPolicyCreateUpdate, + Read: resourceArmApiManagementPolicyRead, + Update: resourceArmApiManagementPolicyCreateUpdate, + Delete: resourceArmApiManagementPolicyDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "resource_group_name": resourceGroupNameSchema(), + + "api_management_name": azure.SchemaApiManagementName(), + + "xml_content": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: suppress.SuppressXmlDiff, + ConflictsWith: []string{"xml_link"}, + }, + + "xml_link": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"xml_content"}, + }, + }, + } +} + +func resourceArmApiManagementPolicyCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).apiManagementPolicyClient + ctx := meta.(*ArmClient).StopContext + + resourceGroup := d.Get("resource_group_name").(string) + serviceName := d.Get("api_management_name").(string) + + if requireResourcesToBeImported { + resp, err := client.Get(ctx, resourceGroup, serviceName) + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Error checking for present of existing Global Policy (API Management Service %q / Resource Group %q): %+v", serviceName, resourceGroup, err) + } + } + if !utils.ResponseWasNotFound(resp.Response) { + return tf.ImportAsExistsError("azurerm_api_management_policy", *resp.ID) + } + } + + xmlContent := d.Get("xml_content").(string) + xmlLink := d.Get("xml_link").(string) + if xmlContent == "" && xmlLink == "" { + return fmt.Errorf("Either `xml_content` or `xml_link` is required") + } + + content := xmlContent + format := apimanagement.XML + if xmlLink != "" { + content = xmlLink + format = apimanagement.XMLLink + } + + parameters := apimanagement.PolicyContract{ + PolicyContractProperties: &apimanagement.PolicyContractProperties{ + ContentFormat: format, + PolicyContent: utils.String(content), + }, + } + + if _, err := client.CreateOrUpdate(ctx, resourceGroup, serviceName, parameters); err != nil { + return fmt.Errorf("Error creating Global Policy (API Management Service %q / Resource Group %q): %+v", serviceName, resourceGroup, err) + } + + resp, err := client.Get(ctx, resourceGroup, serviceName) + if err != nil { + return fmt.Errorf("Error retrieving Global Policy (API Management Service %q / Resource Group %q): %+v", serviceName, resourceGroup, err) + } + if resp.ID == nil { + return fmt.Errorf("Cannot read Global Policy (API Management Service %q / Resource Group %q) ID", serviceName, resourceGroup) + } + d.SetId(*resp.ID) + + return resourceArmApiManagementPolicyRead(d, meta) +} + +func resourceArmApiManagementPolicyRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).apiManagementPolicyClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resourceGroup := id.ResourceGroup + serviceName := id.Path["service"] + + resp, err := client.Get(ctx, resourceGroup, serviceName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] Api Management Policy %q does not exist - removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Error reading Global Policy (API Management Service %q / Resource Group %q): %+v", serviceName, resourceGroup, err) + } + + d.Set("resource_group_name", resourceGroup) + d.Set("api_management_name", serviceName) + if properties := resp.PolicyContractProperties; properties != nil { + d.Set("xml_content", "") + d.Set("xml_link", "") + if properties.ContentFormat == apimanagement.XML { + d.Set("xml_content", properties.PolicyContent) + } else if properties.ContentFormat == apimanagement.XMLLink { + d.Set("xml_link", properties.PolicyContent) + } + } + + return nil +} + +func resourceArmApiManagementPolicyDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).apiManagementPolicyClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resourceGroup := id.ResourceGroup + serviceName := id.Path["service"] + + if _, err := client.Delete(ctx, resourceGroup, serviceName, ""); err != nil { + return fmt.Errorf("Error deleting Global Policy (API Management Service %q / Resource Group %q): %+v", serviceName, resourceGroup, err) + } + + return nil +} diff --git a/azurerm/resource_arm_api_management_policy_test.go b/azurerm/resource_arm_api_management_policy_test.go new file mode 100644 index 000000000000..e4beb92f0ea7 --- /dev/null +++ b/azurerm/resource_arm_api_management_policy_test.go @@ -0,0 +1,202 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMApiManagementPolicy_basic(t *testing.T) { + resourceName := "azurerm_api_management_policy.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMApiManagementPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagementPolicy_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementPolicyExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "xml_content", ""), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMApiManagementPolicy_findReplace(t *testing.T) { + resourceName := "azurerm_api_management_policy.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMApiManagementPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagementPolicy_findReplace(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementPolicyExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "xml_content", "\r\n\t\r\n\t\t\r\n\t\r\n"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMApiManagementPolicy_update(t *testing.T) { + resourceName := "azurerm_api_management_policy.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMApiManagementPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagementPolicy_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementPolicyExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "xml_content", ""), + ), + }, + { + Config: testAccAzureRMApiManagementPolicy_findReplace(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementPolicyExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "xml_content", "\r\n\t\r\n\t\t\r\n\t\r\n"), + ), + }, + { + Config: testAccAzureRMApiManagementPolicy_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementPolicyExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "xml_content", ""), + ), + }, + }, + }) +} + +func testCheckAzureRMApiManagementPolicyExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Api Management Policy not found: %s", resourceName) + } + + resourceGroup := rs.Primary.Attributes["resource_group_name"] + serviceName := rs.Primary.Attributes["api_management_name"] + + client := testAccProvider.Meta().(*ArmClient).apiManagementPolicyClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + if resp, err := client.Get(ctx, resourceGroup, serviceName); err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Global Policy (API Management Service %q / Resource Group %q) does not exist", serviceName, resourceGroup) + } + return fmt.Errorf("Bad: Get on apiManagementPolicyClient: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMApiManagementPolicyDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).apiManagementPolicyClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_api_management_policy" { + continue + } + + resourceGroup := rs.Primary.Attributes["resource_group_name"] + serviceName := rs.Primary.Attributes["api_management_name"] + + if resp, err := client.Get(ctx, resourceGroup, serviceName); err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Get on apiManagementPolicyClient: %+v", err) + } + } + + return nil + } + + return nil +} + +func testAccAzureRMApiManagementPolicy_basic(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_api_management" "test" { + name = "acctestAM-%d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + publisher_name = "pub1" + publisher_email = "pub1@email.com" + + sku = { + name = "Developer" + capacity = 1 + } +} + +resource "azurerm_api_management_policy" "test" { + resource_group_name = "${azurerm_resource_group.test.name}" + api_management_name = "${azurerm_api_management.test.name}" + xml_content = "" +} +`, rInt, location, rInt) +} + +func testAccAzureRMApiManagementPolicy_findReplace(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_api_management" "test" { + name = "acctestAM-%d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + publisher_name = "pub1" + publisher_email = "pub1@email.com" + + sku = { + name = "Developer" + capacity = 1 + } +} + +resource "azurerm_api_management_policy" "test" { + resource_group_name = "${azurerm_resource_group.test.name}" + api_management_name = "${azurerm_api_management.test.name}" + xml_content = "" +} +`, rInt, location, rInt) +} diff --git a/website/azurerm.erb b/website/azurerm.erb index ca9a6b44c279..3962a487c896 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -354,6 +354,10 @@ azurerm_api_management_logger + > + azurerm_api_management_policy + + > azurerm_api_management_product diff --git a/website/docs/r/api_management_policy.html.markdown b/website/docs/r/api_management_policy.html.markdown new file mode 100644 index 000000000000..6790414dbd31 --- /dev/null +++ b/website/docs/r/api_management_policy.html.markdown @@ -0,0 +1,65 @@ +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_api_management_policy" +sidebar_current: "docs-azurerm-resource-api-management-policy" +description: |- + Manages a global Policy within an API Management Service. +--- + +# azurerm_api_management_policy + +Manages a global Policy within an API Management Service. + + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-rg" + location = "West US" +} + +resource "azurerm_api_management" "example" { + name = "example-apim" + resource_group_name = "${azurerm_resource_group.example.name}" + location = "${azurerm_resource_group.example.location}" + publisher_name = "pub1" + publisher_email = "pub1@email.com" + + sku = { + name = "Developer" + capacity = 1 + } +} + +resource "azurerm_api_management_policy" "example" { + resource_group_name = "${azurerm_resource_group.example.name}" + api_management_name = "${azurerm_api_management.example.name}" + xml_content = "" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `resource_group_name` - (Required) The name of the Resource Group in which the API Management Service exists. Changing this forces a new resource to be created. + +* `api_management_name` - (Required) The name of the API Management Service where this Policy should be created. Changing this forces a new resource to be created. + +* `xml_content` - (Optional) The XML configuration of this Policy. + +* `xml_link` - (Optional) The HTTP endpoint of the XML configuration accessible from the API Management Service. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the API Management Policy. + + +## Import + +API Management Policy can be imported using the `resource id`, e.g. +```shell +$ terraform import azurerm_api_management_policy.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/example-rg/Microsoft.ApiManagement/service/example-apim/policies/policy +```