diff --git a/azurerm/config.go b/azurerm/config.go index e191f31f6e88..7b8cd7c6f67a 100644 --- a/azurerm/config.go +++ b/azurerm/config.go @@ -135,6 +135,7 @@ type ArmClient struct { apiManagementGroupUsersClient apimanagement.GroupUserClient apiManagementLoggerClient apimanagement.LoggerClient apiManagementOpenIdConnectClient apimanagement.OpenIDConnectProviderClient + apiManagementPolicyClient apimanagement.PolicyClient apiManagementProductsClient apimanagement.ProductClient apiManagementProductApisClient apimanagement.ProductAPIClient apiManagementProductGroupsClient apimanagement.ProductGroupClient @@ -532,6 +533,10 @@ func (c *ArmClient) registerApiManagementServiceClients(endpoint, subscriptionId c.configureClient(&loggerClient.Client, auth) c.apiManagementLoggerClient = loggerClient + policyClient := apimanagement.NewPolicyClientWithBaseURI(endpoint, subscriptionId) + c.configureClient(&policyClient.Client, auth) + c.apiManagementPolicyClient = policyClient + serviceClient := apimanagement.NewServiceClientWithBaseURI(endpoint, subscriptionId) c.configureClient(&serviceClient.Client, auth) c.apiManagementServiceClient = serviceClient 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/resource_arm_api_management.go b/azurerm/resource_arm_api_management.go index 5825aeebae0a..5e5ef8d5fc4f 100644 --- a/azurerm/resource_arm_api_management.go +++ b/azurerm/resource_arm_api_management.go @@ -255,6 +255,27 @@ func resourceArmApiManagementService() *schema.Resource { }, }, + "policy": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "xml_content": { + Type: schema.TypeString, + Required: true, + ConflictsWith: []string{"policy.0.xml_link"}, + }, + + "xml_link": { + Type: schema.TypeString, + Required: true, + ConflictsWith: []string{"policy.0.xml_content"}, + }, + }, + }, + }, + "sign_in": { Type: schema.TypeList, Optional: true, @@ -431,6 +452,26 @@ func resourceArmApiManagementServiceCreateUpdate(d *schema.ResourceData, meta in return fmt.Errorf("Error setting Sign Up settings for API Management Service %q (Resource Group %q): %+v", name, resourceGroup, err) } + policyClient := meta.(*ArmClient).apiManagementPolicyClient + policiesRaw := d.Get("policy").([]interface{}) + policy, err := expandApiManagementPolicies(policiesRaw) + if err != nil { + return err + } + + if policy != nil { + if _, err := policyClient.CreateOrUpdate(ctx, resourceGroup, name, *policy); err != nil { + return fmt.Errorf("Error setting Policies for API Management Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + } else { + // reset them + if resp, err := policyClient.Delete(ctx, resourceGroup, name, ""); err != nil { + if !utils.ResponseWasNotFound(resp) { + return fmt.Errorf("Error removing Policies from API Management Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + } + } + return resourceArmApiManagementServiceRead(d, meta) } @@ -469,6 +510,14 @@ func resourceArmApiManagementServiceRead(d *schema.ResourceData, meta interface{ return fmt.Errorf("Error retrieving Sign Up Settings for API Management Service %q (Resource Group %q): %+v", name, resourceGroup, err) } + policyClient := meta.(*ArmClient).apiManagementPolicyClient + policy, err := policyClient.Get(ctx, resourceGroup, name) + if err != nil { + if !utils.ResponseWasNotFound(policy.Response) { + return fmt.Errorf("Error retrieving Policy for API Management Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + } + d.Set("name", name) d.Set("resource_group_name", resourceGroup) @@ -520,6 +569,10 @@ func resourceArmApiManagementServiceRead(d *schema.ResourceData, meta interface{ flattenAndSetTags(d, resp.Tags) + if err := d.Set("policy", flattenApiManagementPolicies(policy)); err != nil { + return fmt.Errorf("Error setting `policy`: %+v", err) + } + return nil } @@ -1045,3 +1098,57 @@ func flattenApiManagementSignUpSettings(input apimanagement.PortalSignupSettings }, } } + +func expandApiManagementPolicies(input []interface{}) (*apimanagement.PolicyContract, error) { + if len(input) == 0 { + return nil, nil + } + + vs := input[0].(map[string]interface{}) + xmlContent := vs["xml_content"].(string) + xmlLink := vs["xml_link"].(string) + + if xmlContent != "" { + return &apimanagement.PolicyContract{ + PolicyContractProperties: &apimanagement.PolicyContractProperties{ + ContentFormat: apimanagement.XML, + PolicyContent: utils.String(xmlContent), + }, + }, nil + } + + if xmlLink != "" { + return &apimanagement.PolicyContract{ + PolicyContractProperties: &apimanagement.PolicyContractProperties{ + ContentFormat: apimanagement.XMLLink, + PolicyContent: utils.String(xmlLink), + }, + }, nil + } + + return nil, fmt.Errorf("Either `xml_content` or `xml_link` should be set if the `policy` block is defined.") +} + +func flattenApiManagementPolicies(input apimanagement.PolicyContract) []interface{} { + output := map[string]interface{}{ + "xml_content": "", + "xml_link": "", + } + + if props := input.PolicyContractProperties; props != nil { + if props.PolicyContent == nil { + return []interface{}{output} + } + + switch props.ContentFormat { + case apimanagement.XML: + output["xml_content"] = *props.PolicyContent + case apimanagement.XMLLink: + output["xml_link"] = *props.PolicyContent + default: + log.Printf("[DEBUG] Unsupported Content Format %q for Policy", string(props.ContentFormat)) + } + } + + return []interface{}{output} +} diff --git a/azurerm/resource_arm_api_management_test.go b/azurerm/resource_arm_api_management_test.go index cac05837df60..f8b8251b69e5 100644 --- a/azurerm/resource_arm_api_management_test.go +++ b/azurerm/resource_arm_api_management_test.go @@ -150,6 +150,53 @@ func TestAccAzureRMApiManagement_signInSignUpSettings(t *testing.T) { }) } +func TestAccAzureRMApiManagement_policy(t *testing.T) { + resourceName := "azurerm_api_management.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_policyXmlContent(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAzureRMApiManagement_policyXmlLink(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAzureRMApiManagement_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testCheckAzureRMApiManagementDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*ArmClient).apiManagementServiceClient @@ -228,6 +275,64 @@ resource "azurerm_api_management" "test" { `, rInt, location, rInt) } +func testAccAzureRMApiManagement_policyXmlContent(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" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + publisher_name = "pub1" + publisher_email = "pub1@email.com" + + sku { + name = "Developer" + capacity = 1 + } + + policy { + xml_content = < + + + + +XML + } +} +`, rInt, location, rInt) +} + +func testAccAzureRMApiManagement_policyXmlLink(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" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + publisher_name = "pub1" + publisher_email = "pub1@email.com" + + sku { + name = "Developer" + capacity = 1 + } + + policy { + xml_link = "https://gist.githubusercontent.com/tombuildsstuff/4f58581599d2c9f64b236f505a361a67/raw/0d29dcb0167af1e5afe4bd52a6d7f69ba1e05e1f/example.xml" + } +} +`, rInt, location, rInt) +} + func testAccAzureRMApiManagement_requiresImport(rInt int, location string) string { template := testAccAzureRMApiManagement_basic(rInt, location) return fmt.Sprintf(`