Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New resource: azurerm_api_management_custom_domain #8228

Merged
merged 28 commits into from
Nov 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
139e55d
Add `azurerm_api_management_custom_domain` resource
sirlatrom Aug 24, 2020
7ef6c06
Apply suggestions from code review
sirlatrom Sep 25, 2020
d49e6dd
Moved apiManagementResourceHostnameSchema and apiManagementResourceHo…
sirlatrom Sep 25, 2020
852d642
Complete basic tests and fill out TODOs in docs
sirlatrom Sep 25, 2020
5cd66de
Combine similar blocks in docs
sirlatrom Sep 25, 2020
51b453c
Fix panic if hostname_configuration is not filled out
sirlatrom Sep 25, 2020
2b53077
Only set HostnameConfigurations if configured
sirlatrom Sep 25, 2020
6a9a9e1
Add separate expand func for custom domains
sirlatrom Sep 25, 2020
67658bb
Fix types of domain blocks when expanding
sirlatrom Sep 25, 2020
32c5132
Adress PR comments
sirlatrom Sep 30, 2020
6e3ab6d
Fix formatting of terraform config in test
sirlatrom Sep 30, 2020
656071e
Check err
sirlatrom Sep 30, 2020
4ed2dd0
Make sure to use built-in ResourceGroup field on id
sirlatrom Oct 1, 2020
6049da8
Add ToSnakeCase function
sirlatrom Oct 7, 2020
243fa79
Add ID parser for azurerm_api_management_custom_domain
sirlatrom Oct 7, 2020
9e279c0
Add custom parse function for API Management service
sirlatrom Oct 20, 2020
477b241
Collateral fix in test failure output
sirlatrom Oct 20, 2020
342994b
Apply suggestions from code review
sirlatrom Oct 20, 2020
d209a64
Add update test case
sirlatrom Oct 20, 2020
2f7a335
Fix up tests for api_management_custom_domain
manicminer Oct 20, 2020
a21d9a4
If hostname_configurations is available, it will always contain a def…
sirlatrom Oct 23, 2020
ecb08b2
Unused attribute
manicminer Nov 2, 2020
8ef78df
Assign key_permisions.get on keyvault for test SP
manicminer Nov 4, 2020
3613e20
Consistency and testing fixes
manicminer Nov 5, 2020
f81b49c
Wait for state change after updating
manicminer Nov 5, 2020
d7c9e67
Linting, up the state checking count for a little extra safety
manicminer Nov 5, 2020
33cd204
Hostnames are equivalent regardless of case
manicminer Nov 5, 2020
573b977
Simplify attribute key comparison
manicminer Nov 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions azurerm/helpers/azure/api_management.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,20 @@ func FlattenApiManagementOperationParameterContract(input *[]apimanagement.Param

return outputs
}

// CopyCertificateAndPassword copies any certificate and password attributes
// from the old config to the current to avoid state diffs.
// Iterate through old state to find sensitive props not returned by API.
// This must be done in order to avoid state diffs.
// NOTE: this information won't be available during times like Import, so this is a best-effort.
func CopyCertificateAndPassword(vals []interface{}, hostName string, output map[string]interface{}) {
for _, val := range vals {
oldConfig := val.(map[string]interface{})

if oldConfig["host_name"] == hostName {
output["certificate_password"] = oldConfig["certificate_password"]
output["certificate"] = oldConfig["certificate"]
break
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,377 @@
package apimanagement

import (
"fmt"
"log"
"strings"
"time"

"github.com/Azure/azure-sdk-for-go/services/apimanagement/mgmt/2019-12-01/apimanagement"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"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/apimanagement/parse"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

var apiManagementCustomDomainResourceName = "azurerm_api_management_custom_domain"

func resourceArmApiManagementCustomDomain() *schema.Resource {
return &schema.Resource{
Create: apiManagementCustomDomainCreateUpdate,
Read: apiManagementCustomDomainRead,
Update: apiManagementCustomDomainCreateUpdate,
Delete: apiManagementCustomDomainDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(30 * time.Minute),
Read: schema.DefaultTimeout(5 * time.Minute),
Update: schema.DefaultTimeout(30 * time.Minute),
Delete: schema.DefaultTimeout(30 * time.Minute),
},

Schema: map[string]*schema.Schema{
"api_management_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: azure.ValidateResourceID,
},

"management": {
Type: schema.TypeList,
Optional: true,
AtLeastOneOf: []string{"management", "portal", "developer_portal", "proxy", "scm"},
Elem: &schema.Resource{
Schema: apiManagementResourceHostnameSchema(),
sirlatrom marked this conversation as resolved.
Show resolved Hide resolved
},
},
"portal": {
Type: schema.TypeList,
Optional: true,
AtLeastOneOf: []string{"management", "portal", "developer_portal", "proxy", "scm"},
Elem: &schema.Resource{
Schema: apiManagementResourceHostnameSchema(),
},
},
"developer_portal": {
Type: schema.TypeList,
Optional: true,
AtLeastOneOf: []string{"management", "portal", "developer_portal", "proxy", "scm"},
Elem: &schema.Resource{
Schema: apiManagementResourceHostnameSchema(),
},
},
"proxy": {
Type: schema.TypeList,
Optional: true,
AtLeastOneOf: []string{"management", "portal", "developer_portal", "proxy", "scm"},
Elem: &schema.Resource{
Schema: apiManagementResourceHostnameProxySchema(),
},
},
"scm": {
Type: schema.TypeList,
Optional: true,
AtLeastOneOf: []string{"management", "portal", "developer_portal", "proxy", "scm"},
Elem: &schema.Resource{
Schema: apiManagementResourceHostnameSchema(),
},
},
},
}
}

func apiManagementCustomDomainCreateUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).ApiManagement.ServiceClient
ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d)
defer cancel()

log.Printf("[INFO] preparing arguments for API Management Custom domain creation.")

apiManagementID := d.Get("api_management_id").(string)
id, err := parse.ApiManagementID(apiManagementID)
if err != nil {
return err
}
resourceGroup := id.ResourceGroup
serviceName := id.ServiceName

existing, err := client.Get(ctx, resourceGroup, serviceName)
if err != nil {
return fmt.Errorf("finding API Management (API Management %q / Resource Group %q): %s", serviceName, resourceGroup, err)
}

if d.IsNewResource() {
if existing.ServiceProperties != nil && existing.ServiceProperties.HostnameConfigurations != nil && len(*existing.ServiceProperties.HostnameConfigurations) > 1 {
return tf.ImportAsExistsError(apiManagementCustomDomainResourceName, *existing.ID)
}
}

existing.ServiceProperties.HostnameConfigurations = expandApiManagementCustomDomains(d)

// Wait for the ProvisioningState to become "Succeeded" before attempting to update
log.Printf("[DEBUG] Waiting for API Management Service %q (Resource Group: %q) to become ready", serviceName, resourceGroup)
stateConf := &resource.StateChangeConf{
Pending: []string{"Updating", "Unknown"},
Target: []string{"Succeeded", "Ready"},
Refresh: apiManagementRefreshFunc(ctx, client, serviceName, resourceGroup),
MinTimeout: 1 * time.Minute,
ContinuousTargetOccurence: 6,
}
if d.IsNewResource() {
stateConf.Timeout = d.Timeout(schema.TimeoutCreate)
} else {
stateConf.Timeout = d.Timeout(schema.TimeoutUpdate)
}

if _, err = stateConf.WaitForState(); err != nil {
return fmt.Errorf("waiting for API Management Service %q (Resource Group: %q) to become ready: %+v", serviceName, resourceGroup, err)
}

if _, err := client.CreateOrUpdate(ctx, resourceGroup, serviceName, existing); err != nil {
return fmt.Errorf("creating/updating Custom Domain (API Management %q / Resource Group %q): %+v", serviceName, resourceGroup, err)
}

read, err := client.Get(ctx, resourceGroup, serviceName)
if err != nil {
return fmt.Errorf("retrieving Custom Domain (API Management %q / Resource Group %q): %+v", serviceName, resourceGroup, err)
}
if read.ID == nil {
return fmt.Errorf("cannot read ID for Custom Domain (API Management %q / Resource Group %q)", serviceName, resourceGroup)
}

// Wait for the ProvisioningState to become "Succeeded" before attempting to update
log.Printf("[DEBUG] Waiting for API Management Service %q (Resource Group: %q) to become ready", serviceName, resourceGroup)
if _, err = stateConf.WaitForState(); err != nil {
return fmt.Errorf("waiting for API Management Service %q (Resource Group: %q) to become ready: %+v", serviceName, resourceGroup, err)
}

customDomainsID := fmt.Sprintf("%s/customDomains/default", *read.ID)
d.SetId(customDomainsID)

return apiManagementCustomDomainRead(d, meta)
}

func apiManagementCustomDomainRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).ApiManagement.ServiceClient
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
defer cancel()

id, err := parse.ApiManagementCustomDomainID(d.Id())
if err != nil {
return err
}
resourceGroup := id.ResourceGroup
serviceName := id.ServiceName

resp, err := client.Get(ctx, resourceGroup, serviceName)
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
log.Printf("API Management Service %q was not found in Resource Group %q - removing from state!", serviceName, resourceGroup)
d.SetId("")
return nil
}

return fmt.Errorf("making Read request on API Management Service %q (Resource Group %q): %+v", serviceName, resourceGroup, err)
}

d.Set("api_management_id", resp.ID)

if resp.ServiceProperties != nil && resp.ServiceProperties.HostnameConfigurations != nil {
configs := flattenApiManagementHostnameConfiguration(resp.ServiceProperties.HostnameConfigurations, d)
for _, config := range configs {
for key, v := range config.(map[string]interface{}) {
if err := d.Set(key, v); err != nil {
return fmt.Errorf("setting `hostname_configuration` %q: %+v", key, err)
}
}
}
}

return nil
}

func apiManagementCustomDomainDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).ApiManagement.ServiceClient
ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d)
defer cancel()

id, err := parse.ApiManagementCustomDomainID(d.Id())
if err != nil {
return err
}
resourceGroup := id.ResourceGroup
serviceName := id.ServiceName

resp, err := client.Get(ctx, resourceGroup, serviceName)
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
log.Printf("API Management Service %q was not found in Resource Group %q - removing from state!", serviceName, resourceGroup)
d.SetId("")
return nil
}

return fmt.Errorf("making Read request on API Management Service %q (Resource Group %q): %+v", serviceName, resourceGroup, err)
}

// Wait for the ProvisioningState to become "Succeeded" before attempting to update
log.Printf("[DEBUG] Waiting for API Management Service %q (Resource Group: %q) to become ready", serviceName, resourceGroup)
stateConf := &resource.StateChangeConf{
Pending: []string{"Updating", "Unknown"},
Target: []string{"Succeeded", "Ready"},
Refresh: apiManagementRefreshFunc(ctx, client, serviceName, resourceGroup),
MinTimeout: 1 * time.Minute,
Timeout: d.Timeout(schema.TimeoutDelete),
ContinuousTargetOccurence: 6,
}

if _, err = stateConf.WaitForState(); err != nil {
return fmt.Errorf("waiting for API Management Service %q (Resource Group: %q) to become ready: %+v", serviceName, resourceGroup, err)
}

log.Printf("[DEBUG] Deleting API Management Custom Domain (API Management %q / Resource Group %q)", serviceName, resourceGroup)

resp.ServiceProperties.HostnameConfigurations = nil

if _, err := client.CreateOrUpdate(ctx, resourceGroup, serviceName, resp); err != nil {
return fmt.Errorf("deleting Custom Domain (API Management %q / Resource Group %q): %+v", serviceName, resourceGroup, err)
}

// Wait for the ProvisioningState to become "Succeeded" before attempting to update
log.Printf("[DEBUG] Waiting for API Management Service %q (Resource Group: %q) to become ready", serviceName, resourceGroup)
if _, err = stateConf.WaitForState(); err != nil {
return fmt.Errorf("waiting for API Management Service %q (Resource Group: %q) to become ready: %+v", serviceName, resourceGroup, err)
}

return nil
}

func expandApiManagementCustomDomains(input *schema.ResourceData) *[]apimanagement.HostnameConfiguration {
results := make([]apimanagement.HostnameConfiguration, 0)

if managementRawVal, ok := input.GetOk("management"); ok {
vs := managementRawVal.([]interface{})
for _, rawVal := range vs {
v := rawVal.(map[string]interface{})
output := expandApiManagementCommonHostnameConfiguration(v, apimanagement.HostnameTypeManagement)
results = append(results, output)
}
}
if portalRawVal, ok := input.GetOk("portal"); ok {
vs := portalRawVal.([]interface{})
for _, rawVal := range vs {
v := rawVal.(map[string]interface{})
output := expandApiManagementCommonHostnameConfiguration(v, apimanagement.HostnameTypePortal)
results = append(results, output)
}
}
if developerPortalRawVal, ok := input.GetOk("developer_portal"); ok {
vs := developerPortalRawVal.([]interface{})
for _, rawVal := range vs {
v := rawVal.(map[string]interface{})
output := expandApiManagementCommonHostnameConfiguration(v, apimanagement.HostnameTypeDeveloperPortal)
results = append(results, output)
}
}
if proxyRawVal, ok := input.GetOk("proxy"); ok {
vs := proxyRawVal.([]interface{})
for _, rawVal := range vs {
v := rawVal.(map[string]interface{})
output := expandApiManagementCommonHostnameConfiguration(v, apimanagement.HostnameTypeProxy)
if value, ok := v["default_ssl_binding"]; ok {
output.DefaultSslBinding = utils.Bool(value.(bool))
}
results = append(results, output)
}
}
if scmRawVal, ok := input.GetOk("scm"); ok {
vs := scmRawVal.([]interface{})
for _, rawVal := range vs {
v := rawVal.(map[string]interface{})
output := expandApiManagementCommonHostnameConfiguration(v, apimanagement.HostnameTypeScm)
results = append(results, output)
}
}
return &results
}

func flattenApiManagementHostnameConfiguration(input *[]apimanagement.HostnameConfiguration, d *schema.ResourceData) []interface{} {
results := make([]interface{}, 0)
if input == nil {
return results
}

managementResults := make([]interface{}, 0)
portalResults := make([]interface{}, 0)
developerPortalResults := make([]interface{}, 0)
proxyResults := make([]interface{}, 0)
scmResults := make([]interface{}, 0)

for _, config := range *input {
output := make(map[string]interface{})

if config.HostName != nil {
output["host_name"] = *config.HostName
}

if config.NegotiateClientCertificate != nil {
output["negotiate_client_certificate"] = *config.NegotiateClientCertificate
}

if config.KeyVaultID != nil {
output["key_vault_id"] = *config.KeyVaultID
}

var configType string
switch strings.ToLower(string(config.Type)) {
case strings.ToLower(string(apimanagement.HostnameTypeProxy)):
// only set SSL binding for proxy types
if config.DefaultSslBinding != nil {
output["default_ssl_binding"] = *config.DefaultSslBinding
}
proxyResults = append(proxyResults, output)
configType = "proxy"

case strings.ToLower(string(apimanagement.HostnameTypeManagement)):
managementResults = append(managementResults, output)
configType = "management"

case strings.ToLower(string(apimanagement.HostnameTypePortal)):
portalResults = append(portalResults, output)
configType = "portal"

case strings.ToLower(string(apimanagement.HostnameTypeDeveloperPortal)):
developerPortalResults = append(developerPortalResults, output)
configType = "developer_portal"

case strings.ToLower(string(apimanagement.HostnameTypeScm)):
scmResults = append(scmResults, output)
configType = "scm"
}

if configType != "" {
if valsRaw, ok := d.GetOk(configType); ok {
vals := valsRaw.([]interface{})
azure.CopyCertificateAndPassword(vals, *config.HostName, output)
}
}
}

return []interface{}{
map[string]interface{}{
"management": managementResults,
"portal": portalResults,
"developer_portal": developerPortalResults,
"proxy": proxyResults,
"scm": scmResults,
},
}
}
Loading