diff --git a/internal/services/apimanagement/api_management_api_resource.go b/internal/services/apimanagement/api_management_api_resource.go index 836f5e952425..9f75d8264bf5 100644 --- a/internal/services/apimanagement/api_management_api_resource.go +++ b/internal/services/apimanagement/api_management_api_resource.go @@ -26,9 +26,9 @@ import ( func resourceApiManagementApi() *pluginsdk.Resource { resource := &pluginsdk.Resource{ - Create: resourceApiManagementApiCreateUpdate, + Create: resourceApiManagementApiCreate, Read: resourceApiManagementApiRead, - Update: resourceApiManagementApiCreateUpdate, + Update: resourceApiManagementApiUpdate, Delete: resourceApiManagementApiDelete, Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error { _, err := api.ParseApiID(id) @@ -339,7 +339,7 @@ func resourceApiManagementApi() *pluginsdk.Resource { return resource } -func resourceApiManagementApiCreateUpdate(d *pluginsdk.ResourceData, meta interface{}) error { +func resourceApiManagementApiCreate(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).ApiManagement.ApiClient subscriptionId := meta.(*clients.Client).Account.SubscriptionId ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) @@ -355,8 +355,6 @@ func resourceApiManagementApiCreateUpdate(d *pluginsdk.ResourceData, meta interf protocols := expandApiManagementApiProtocols(protocolsRaw) sourceApiId := d.Get("source_api_id").(string) - id := api.NewApiID(subscriptionId, d.Get("resource_group_name").(string), d.Get("api_management_name").(string), apiId) - if version != "" && versionSetId == "" { return errors.New("setting `version` without the required `version_set_id`") } @@ -365,35 +363,26 @@ func resourceApiManagementApiCreateUpdate(d *pluginsdk.ResourceData, meta interf return errors.New("`display_name`, `protocols` are required when `source_api_id` is not set") } - newId := api.NewApiID(subscriptionId, d.Get("resource_group_name").(string), d.Get("api_management_name").(string), apiId) - if d.IsNewResource() { - existing, err := client.Get(ctx, newId) - if err != nil { - if !response.WasNotFound(existing.HttpResponse) { - return fmt.Errorf("checking for presence of an existing %s: %+v", newId, err) - } - } + id := api.NewApiID(subscriptionId, d.Get("resource_group_name").(string), d.Get("api_management_name").(string), apiId) + existing, err := client.Get(ctx, id) + if err != nil { if !response.WasNotFound(existing.HttpResponse) { - return tf.ImportAsExistsError("azurerm_api_management_api", newId.ID()) + return fmt.Errorf("checking for presence of an existing %s: %+v", id, err) } } + if !response.WasNotFound(existing.HttpResponse) { + return tf.ImportAsExistsError("azurerm_api_management_api", id.ID()) + } apiType := api.ApiTypeHTTP if v, ok := d.GetOk("api_type"); ok { apiType = api.ApiType(v.(string)) } - - soapApiType := map[api.ApiType]api.SoapApiType{ - api.ApiTypeGraphql: api.SoapApiTypeGraphql, - api.ApiTypeHTTP: api.SoapApiTypeHTTP, - api.ApiTypeSoap: api.SoapApiTypeSoap, - api.ApiTypeWebsocket: api.SoapApiTypeWebsocket, - }[apiType] + soapApiType := soapApiTypeFromApiType(apiType) // If import is used, we need to send properties to Azure API in two operations. // First we execute import and then updated the other props. - if vs, hasImport := d.GetOk("import"); hasImport { - importVs := vs.([]interface{}) + if importVs, ok := d.Get("import").([]interface{}); ok && len(importVs) > 0 { importV := importVs[0].(map[string]interface{}) contentFormat := importV["content_format"].(string) contentValue := importV["content_value"].(string) @@ -433,7 +422,7 @@ func resourceApiManagementApiCreateUpdate(d *pluginsdk.ResourceData, meta interf if versionSetId != "" { apiParams.Properties.ApiVersionSetId = pointer.To(versionSetId) } - if err := client.CreateOrUpdateThenPoll(ctx, newId, apiParams, api.CreateOrUpdateOperationOptions{}); err != nil { + if err := client.CreateOrUpdateThenPoll(ctx, id, apiParams, api.CreateOrUpdateOperationOptions{}); err != nil { return fmt.Errorf("creating/updating %s: %+v", id, err) } } @@ -498,7 +487,7 @@ func resourceApiManagementApiCreateUpdate(d *pluginsdk.ResourceData, meta interf params.Properties.TermsOfServiceURL = pointer.To(v.(string)) } - if err := client.CreateOrUpdateThenPoll(ctx, newId, params, api.CreateOrUpdateOperationOptions{IfMatch: pointer.To("*")}); err != nil { + if err := client.CreateOrUpdateThenPoll(ctx, id, params, api.CreateOrUpdateOperationOptions{IfMatch: pointer.To("*")}); err != nil { return fmt.Errorf("creating/updating %s: %+v", id, err) } @@ -506,6 +495,176 @@ func resourceApiManagementApiCreateUpdate(d *pluginsdk.ResourceData, meta interf return resourceApiManagementApiRead(d, meta) } +func resourceApiManagementApiUpdate(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).ApiManagement.ApiClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + path := d.Get("path").(string) + version := d.Get("version").(string) + versionSetId := d.Get("version_set_id").(string) + displayName := d.Get("display_name").(string) + protocolsRaw := d.Get("protocols").(*pluginsdk.Set).List() + protocols := expandApiManagementApiProtocols(protocolsRaw) + sourceApiId := d.Get("source_api_id").(string) + serviceUrl := d.Get("service_url").(string) + + if version != "" && versionSetId == "" { + return errors.New("setting `version` without the required `version_set_id`") + } + + if sourceApiId == "" && (displayName == "" || protocols == nil || len(*protocols) == 0) { + return errors.New("`display_name`, `protocols` are required when `source_api_id` is not set") + } + + id, err := api.ParseApiID(d.Id()) + if err != nil { + return err + } + + apiType := api.ApiTypeHTTP + if v, ok := d.GetOk("api_type"); ok { + apiType = api.ApiType(v.(string)) + } + soapApiType := soapApiTypeFromApiType(apiType) + + // If import is used, we need to send properties to Azure API in two operations. + // First we execute import and then updated the other props. + if d.HasChange("import") { + if vs, hasImport := d.GetOk("import"); hasImport { + apiParams := expandApiManagementApiImport(vs.([]interface{}), apiType, soapApiType, + path, serviceUrl, version, versionSetId) + + if err := client.CreateOrUpdateThenPoll(ctx, *id, apiParams, api.CreateOrUpdateOperationOptions{}); err != nil { + return fmt.Errorf("creating/updating %s: %+v", *id, err) + } + } + } + + resp, err := client.Get(ctx, *id) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + if resp.Model == nil || resp.Model.Properties == nil { + return fmt.Errorf("retrieving %s: `properties` was nil", *id) + } + + existing := resp.Model.Properties + prop := &api.ApiCreateOrUpdateProperties{ + Path: existing.Path, + Protocols: existing.Protocols, + ServiceURL: existing.ServiceURL, + Description: existing.Description, + ApiVersionDescription: existing.ApiVersionDescription, + ApiRevisionDescription: existing.ApiRevisionDescription, + SubscriptionRequired: existing.SubscriptionRequired, + SubscriptionKeyParameterNames: existing.SubscriptionKeyParameterNames, + AuthenticationSettings: existing.AuthenticationSettings, + Contact: existing.Contact, + License: existing.License, + SourceApiId: existing.SourceApiId, + DisplayName: existing.DisplayName, + ApiVersion: existing.ApiVersion, + ApiVersionSetId: existing.ApiVersionSetId, + TermsOfServiceURL: existing.TermsOfServiceURL, + Type: existing.Type, + } + + if d.HasChange("path") { + prop.Path = path + } + + if d.HasChange("protocols") { + prop.Protocols = protocols + } + + if d.HasChange("api_type") { + prop.Type = pointer.To(apiType) + prop.ApiType = pointer.To(soapApiType) + } + + if d.HasChange("service_url") { + prop.ServiceURL = pointer.To(serviceUrl) + } + + if d.HasChange("description") { + prop.Description = pointer.To(d.Get("description").(string)) + } + + if d.HasChange("revision_description") { + prop.ApiRevisionDescription = pointer.To(d.Get("revision_description").(string)) + } + + if d.HasChange("version_description") { + prop.ApiVersionDescription = pointer.To(d.Get("version_description").(string)) + } + if d.HasChange("subscription_required") { + prop.SubscriptionRequired = pointer.To(d.Get("subscription_required").(bool)) + } + + if d.HasChange("subscription_key_parameter_names") { + subscriptionKeyParameterNamesRaw := d.Get("subscription_key_parameter_names").([]interface{}) + prop.SubscriptionKeyParameterNames = expandApiManagementApiSubscriptionKeyParamNames(subscriptionKeyParameterNamesRaw) + } + + authenticationSettings := &api.AuthenticationSettingsContract{} + if d.HasChange("oauth2_authorization") { + oAuth2AuthorizationSettingsRaw := d.Get("oauth2_authorization").([]interface{}) + oAuth2AuthorizationSettings := expandApiManagementOAuth2AuthenticationSettingsContract(oAuth2AuthorizationSettingsRaw) + authenticationSettings.OAuth2 = oAuth2AuthorizationSettings + prop.AuthenticationSettings = authenticationSettings + } + + if d.HasChange("openid_authentication") { + openIDAuthorizationSettingsRaw := d.Get("openid_authentication").([]interface{}) + openIDAuthorizationSettings := expandApiManagementOpenIDAuthenticationSettingsContract(openIDAuthorizationSettingsRaw) + authenticationSettings.Openid = openIDAuthorizationSettings + prop.AuthenticationSettings = authenticationSettings + } + + if d.HasChange("contact") { + contactInfoRaw := d.Get("contact").([]interface{}) + prop.Contact = expandApiManagementApiContact(contactInfoRaw) + } + + if d.HasChange("license") { + licenseInfoRaw := d.Get("license").([]interface{}) + prop.License = expandApiManagementApiLicense(licenseInfoRaw) + } + + if d.HasChange("source_api_id") { + prop.SourceApiId = pointer.To(sourceApiId) + } + + if d.HasChange("display_name") { + prop.DisplayName = pointer.To(displayName) + } + + if d.HasChange("version") { + prop.ApiVersion = pointer.To(version) + } + + if d.HasChange("version_set_id") { + prop.ApiVersionSetId = &versionSetId + } + + if d.HasChange("terms_of_service_url") { + prop.TermsOfServiceURL = pointer.To(d.Get("terms_of_service_url").(string)) + } + + params := api.ApiCreateOrUpdateParameter{ + Properties: prop, + } + + if err := client.CreateOrUpdateThenPoll(ctx, *id, params, api.CreateOrUpdateOperationOptions{IfMatch: pointer.To("*")}); err != nil { + return fmt.Errorf("creating/updating %s: %+v", *id, err) + } + + d.SetId(id.ID()) + return resourceApiManagementApiRead(d, meta) +} + func resourceApiManagementApiRead(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).ApiManagement.ApiClient ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) @@ -599,6 +758,56 @@ func resourceApiManagementApiDelete(d *pluginsdk.ResourceData, meta interface{}) return nil } +func soapApiTypeFromApiType(apiType api.ApiType) api.SoapApiType { + return map[api.ApiType]api.SoapApiType{ + api.ApiTypeGraphql: api.SoapApiTypeGraphql, + api.ApiTypeHTTP: api.SoapApiTypeHTTP, + api.ApiTypeSoap: api.SoapApiTypeSoap, + api.ApiTypeWebsocket: api.SoapApiTypeWebsocket, + }[apiType] +} + +func expandApiManagementApiImport(importVs []interface{}, apiType api.ApiType, soapApiType api.SoapApiType, path, serviceUrl, version, versionSetId string) api.ApiCreateOrUpdateParameter { + importV := importVs[0].(map[string]interface{}) + contentFormat := importV["content_format"].(string) + contentValue := importV["content_value"].(string) + + apiParams := api.ApiCreateOrUpdateParameter{ + Properties: &api.ApiCreateOrUpdateProperties{ + Type: pointer.To(apiType), + ApiType: pointer.To(soapApiType), + Format: pointer.To(api.ContentFormat(contentFormat)), + Value: pointer.To(contentValue), + Path: path, + }, + } + + wsdlSelectorVs := importV["wsdl_selector"].([]interface{}) + if len(wsdlSelectorVs) > 0 { + wsdlSelectorV := wsdlSelectorVs[0].(map[string]interface{}) + wSvcName := wsdlSelectorV["service_name"].(string) + wEndpName := wsdlSelectorV["endpoint_name"].(string) + + apiParams.Properties.WsdlSelector = &api.ApiCreateOrUpdatePropertiesWsdlSelector{ + WsdlServiceName: pointer.To(wSvcName), + WsdlEndpointName: pointer.To(wEndpName), + } + } + if serviceUrl != "" { + apiParams.Properties.ServiceURL = pointer.To(serviceUrl) + } + + if version != "" { + apiParams.Properties.ApiVersion = pointer.To(version) + } + + if versionSetId != "" { + apiParams.Properties.ApiVersionSetId = pointer.To(versionSetId) + } + + return apiParams +} + func expandApiManagementApiProtocols(input []interface{}) *[]api.Protocol { if len(input) == 0 { return nil diff --git a/internal/services/apimanagement/api_management_api_resource_test.go b/internal/services/apimanagement/api_management_api_resource_test.go index 8e3c6365eb16..ab225dad4845 100644 --- a/internal/services/apimanagement/api_management_api_resource_test.go +++ b/internal/services/apimanagement/api_management_api_resource_test.go @@ -208,20 +208,12 @@ func TestAccApiManagementApi_importSwagger(t *testing.T) { data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.importSwagger(data), + Config: r.importSwagger(data, false), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, - { - ResourceName: data.ResourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{ - // not returned from the API - "import", - }, - }, + data.ImportStep("import"), }) } @@ -236,15 +228,7 @@ func TestAccApiManagementApi_importOpenapi(t *testing.T) { check.That(data.ResourceName).ExistsInAzure(r), ), }, - { - ResourceName: data.ResourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{ - // not returned from the API - "import", - }, - }, + data.ImportStep("import"), }) } @@ -259,30 +243,14 @@ func TestAccApiManagementApi_importSwaggerWithServiceUrl(t *testing.T) { check.That(data.ResourceName).ExistsInAzure(r), ), }, - { - ResourceName: data.ResourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{ - // not returned from the API - "import", - }, - }, + data.ImportStep("import"), { Config: r.importSwaggerWithServiceUrlUpdate(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, - { - ResourceName: data.ResourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{ - // not returned from the API - "import", - }, - }, + data.ImportStep("import"), }) } @@ -297,15 +265,7 @@ func TestAccApiManagementApi_importWsdl(t *testing.T) { check.That(data.ResourceName).ExistsInAzure(r), ), }, - { - ResourceName: data.ResourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{ - // not returned from the API - "import", - }, - }, + data.ImportStep("import"), }) } @@ -320,15 +280,7 @@ func TestAccApiManagementApi_importWsdlWithSelector(t *testing.T) { check.That(data.ResourceName).ExistsInAzure(r), ), }, - { - ResourceName: data.ResourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{ - // not returned from the API - "import", - }, - }, + data.ImportStep("import"), }) } @@ -343,34 +295,33 @@ func TestAccApiManagementApi_importUpdate(t *testing.T) { check.That(data.ResourceName).ExistsInAzure(r), ), }, + data.ImportStep("import"), { - ResourceName: data.ResourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{ - // not returned from the API - "import", - }, - }, - { - Config: r.importSwagger(data), + Config: r.importSwagger(data, true), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, + data.ImportStep("import"), + }) +} + +func TestAccApiManagementApi_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management_api", "test") + r := ApiManagementApiResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ { - ResourceName: data.ResourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{ - // not returned from the API - "import", - }, + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), }, + data.ImportStep(), }) } -func TestAccApiManagementApi_complete(t *testing.T) { +func TestAccApiManagementApi_completeUpdate(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_api_management_api", "test") r := ApiManagementApiResource{} @@ -382,6 +333,13 @@ func TestAccApiManagementApi_complete(t *testing.T) { ), }, data.ImportStep(), + { + Config: r.completeUpdate(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), }) } @@ -607,7 +565,15 @@ resource "azurerm_api_management_api" "import" { `, r.basic(data)) } -func (r ApiManagementApiResource) importSwagger(data acceptance.TestData) string { +func (r ApiManagementApiResource) importSwagger(data acceptance.TestData, ignoreImported bool) string { + ignoreConfig := "" + if ignoreImported { + ignoreConfig = `lifecycle { + ignore_changes = [description, display_name, contact, license] +} +` + } + return fmt.Sprintf(` %s @@ -624,8 +590,9 @@ resource "azurerm_api_management_api" "test" { content_value = file("testdata/api_management_api_swagger.json") content_format = "swagger-json" } + %s } -`, r.template(data, SkuNameConsumption), data.RandomInteger) +`, r.template(data, SkuNameConsumption), data.RandomInteger, ignoreConfig) } func (r ApiManagementApiResource) importOpenapi(data acceptance.TestData) string { @@ -778,6 +745,42 @@ resource "azurerm_api_management_api" "test" { `, r.template(data, SkuNameConsumption), data.RandomInteger) } +func (r ApiManagementApiResource) completeUpdate(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_api_management_api" "test" { + name = "acctestapi-%d" + resource_group_name = azurerm_resource_group.test.name + api_management_name = azurerm_api_management.test.name + display_name = "Butter Parser Update" + path = "butter-parser-update" + protocols = ["https"] + revision = "3" + description = "What is my purpose? You parse butter." + service_url = "https://example.com/foo/bar/update" + + subscription_key_parameter_names { + header = "X-Butter-Robot-API-Key" + query = "location-update" + } + + contact { + email = "test-update@test.com" + name = "test-update" + url = "https://example-update:8080" + } + + license { + name = "test-license-update" + url = "https://example:8080/license-update" + } + + terms_of_service_url = "https://example:8080/service-update" +} +`, r.template(data, SkuNameConsumption), data.RandomInteger) +} + func (r ApiManagementApiResource) versionSet(data acceptance.TestData) string { return fmt.Sprintf(` %s diff --git a/website/docs/r/api_management_api.html.markdown b/website/docs/r/api_management_api.html.markdown index f9974036006d..6dd70f45abbe 100644 --- a/website/docs/r/api_management_api.html.markdown +++ b/website/docs/r/api_management_api.html.markdown @@ -73,6 +73,8 @@ The following arguments are supported: * `import` - (Optional) A `import` block as documented below. +-> **NOTE:** The `display_name`, `description`, `contact`, and `license` fields can be imported by the `import` block, which might cause a drift if these fields are set along with the `import` block. + * `license` - (Optional) A `license` block as documented below. * `oauth2_authorization` - (Optional) An `oauth2_authorization` block as documented below.