diff --git a/internal/services/keyvault/key_vault_certificate_contacts_resource.go b/internal/services/keyvault/key_vault_certificate_contacts_resource.go index 0e9a0849645b..d55c25a26b5d 100644 --- a/internal/services/keyvault/key_vault_certificate_contacts_resource.go +++ b/internal/services/keyvault/key_vault_certificate_contacts_resource.go @@ -44,8 +44,7 @@ func (r KeyVaultCertificateContactsResource) Arguments() map[string]*pluginsdk.S "contact": { Type: pluginsdk.TypeSet, - Required: true, - MinItems: 1, + Optional: true, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ "email": { @@ -104,7 +103,7 @@ func (r KeyVaultCertificateContactsResource) Create() sdk.ResourceFunc { keyVaultBaseUri, err := vaultClient.BaseUriForKeyVault(ctx, *keyVaultId) if err != nil { - return fmt.Errorf("looking up Base URI for Key Vault Certificate Contacts from %s: %+v", *keyVaultId, err) + return fmt.Errorf("retrieving Base URI for %s: %+v", *keyVaultId, err) } id, err := parse.NewCertificateContactsID(*keyVaultBaseUri) @@ -117,8 +116,14 @@ func (r KeyVaultCertificateContactsResource) Create() sdk.ResourceFunc { existing, err := client.GetCertificateContacts(ctx, *keyVaultBaseUri) if err != nil { + // If we don't have access to the dataplane due to the public network access + // being disabled just ignore the error and set the ID... + if utils.ResponseWasForbidden(existing.Response) { + return fmt.Errorf("unable to create %s due to data plane access restrictions: %+v", id, err) + } + if !utils.ResponseWasNotFound(existing.Response) { - return fmt.Errorf("checking for presence of existing Certificate Contacts (Key Vault %q): %s", *keyVaultBaseUri, err) + return fmt.Errorf("retrieving existing Certificate Contacts for %s from key vault base URI: %q: %+v", *keyVaultId, *keyVaultBaseUri, err) } } @@ -132,8 +137,16 @@ func (r KeyVaultCertificateContactsResource) Create() sdk.ResourceFunc { ContactList: expandKeyVaultCertificateContactsContact(state.Contact), } - if _, err := client.SetCertificateContacts(ctx, *keyVaultBaseUri, contacts); err != nil { - return fmt.Errorf("creating Key Vault Certificate Contacts %s: %+v", id, err) + // Don't set the contacts unless there are contacts defined... + // but we do need to create the empty resource to track resource drift + // over time, incase contacts were added to the key vault via some other tool + // set (e.g., Azure Portal, Cli, AzAPI, etc.)... + if len(*contacts.ContactList) != 0 { + if _, err := client.SetCertificateContacts(ctx, *keyVaultBaseUri, contacts); err != nil { + return fmt.Errorf("creating Key Vault Certificate Contacts %s: %+v", id, err) + } + } else { + metadata.Logger.Infof("[CREATE] Contacts are empty - NoOp, but still set the ID since the resource was defined in the configuration file") } metadata.SetID(id) @@ -158,12 +171,14 @@ func (r KeyVaultCertificateContactsResource) Read() sdk.ResourceFunc { subscriptionResourceId := commonids.NewSubscriptionID(subscriptionId) keyVaultIdRaw, err := vaultClient.KeyVaultIDFromBaseUrl(ctx, subscriptionResourceId, id.KeyVaultBaseUrl) if err != nil { - return fmt.Errorf("retrieving resource ID of the Key Vault at URL %s: %+v", id.KeyVaultBaseUrl, err) + return fmt.Errorf("retrieving resource ID of the Key Vault from Key Vault Base URL %q in %s: %+v", id.KeyVaultBaseUrl, subscriptionResourceId, err) } + if keyVaultIdRaw == nil { - metadata.Logger.Infof("Unable to determine the Resource ID for the Key Vault at URL %s - removing from state!", id.KeyVaultBaseUrl) + metadata.Logger.Infof("[READ] unable to retrieve Key Vault resource ID from Key Vault Base URL %q in %s - removing from state", id.KeyVaultBaseUrl, subscriptionResourceId) return metadata.MarkAsGone(id) } + keyVaultId, err := commonids.ParseKeyVaultID(*keyVaultIdRaw) if err != nil { return fmt.Errorf("parsing Key Vault ID: %+v", err) @@ -171,11 +186,28 @@ func (r KeyVaultCertificateContactsResource) Read() sdk.ResourceFunc { existing, err := client.GetCertificateContacts(ctx, id.KeyVaultBaseUrl) if err != nil { + // The contacts may or may not have changed but we do not have access to them due + // to the key vault public network access has been disabled... should just exit + // since we have no access to dataplane... This will preserve the contacts + // in the state file so once we do regain access to the dataplane we will be able + // to diff the resources contacts with the key vaults contacts... + if utils.ResponseWasForbidden(existing.Response) { + metadata.Logger.Infof("[READ] unable to enumerate key vault certificate contacts at URL %s in %s - ignore error", id.KeyVaultBaseUrl, subscriptionResourceId) + return fmt.Errorf("unable to read %s due to data plane access restrictions: %+v", keyVaultId.ID(), err) + } + if utils.ResponseWasNotFound(existing.Response) { - metadata.Logger.Infof("No Certificate Contacts could be found at %s - removing from state!", id.KeyVaultBaseUrl) - return metadata.MarkAsGone(id) + metadata.Logger.Infof("[READ] no certificate contacts were returned from key vault URL %s in %s - set empty contact list to state", id.KeyVaultBaseUrl, subscriptionResourceId) + + state := KeyVaultCertificateContactsResourceModel{ + KeyVaultId: keyVaultId.ID(), + Contact: make([]Contact, 0), + } + + return metadata.Encode(&state) } - return fmt.Errorf("checking for presence of existing Certificate Contacts (Key Vault %q): %s", id.KeyVaultBaseUrl, err) + + return fmt.Errorf("retrieving existing Certificate Contacts from key vault base URL: %q in %s: %+v", id.KeyVaultBaseUrl, subscriptionResourceId, err) } state := KeyVaultCertificateContactsResourceModel{ @@ -209,14 +241,34 @@ func (r KeyVaultCertificateContactsResource) Update() sdk.ResourceFunc { existing, err := client.GetCertificateContacts(ctx, id.KeyVaultBaseUrl) if err != nil { - return fmt.Errorf("checking for presence of existing Certificate Contacts (Key Vault %q): %s", id.KeyVaultBaseUrl, err) + // If we don't have access to the dataplane due to the public network access + // being disabled just return and keep the state unchanged... + if utils.ResponseWasForbidden(existing.Response) { + metadata.Logger.Infof("'GetCertificateContacts' error result was 'Forbidden' for key vault URL %s - dataplane access denied, keep current state", id.KeyVaultBaseUrl) + return fmt.Errorf("unable to update %s due to data plane access restrictions: %+v", id.KeyVaultBaseUrl, err) + } + + // Since contacts can now be empty Not Found is no longer a reason to return + // an error... + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("retrieving existing Certificate Contacts from key vault base URL: %q: %+v", id.KeyVaultBaseUrl, err) + } else { + metadata.Logger.Infof("'GetCertificateContacts' error result was 'NotFound' for key vault URL %s - ignore error", id.KeyVaultBaseUrl) + } } if metadata.ResourceData.HasChange("contact") { existing.ContactList = expandKeyVaultCertificateContactsContact(state.Contact) } - if _, err := client.SetCertificateContacts(ctx, id.KeyVaultBaseUrl, existing); err != nil { + var updateErr error + if len(*existing.ContactList) == 0 { + _, updateErr = client.DeleteCertificateContacts(ctx, id.KeyVaultBaseUrl) + } else { + _, updateErr = client.SetCertificateContacts(ctx, id.KeyVaultBaseUrl, existing) + } + + if updateErr != nil { return fmt.Errorf("updating Key Vault Certificate Contacts %s: %+v", id, err) } @@ -239,8 +291,28 @@ func (r KeyVaultCertificateContactsResource) Delete() sdk.ResourceFunc { locks.ByID(id.ID()) defer locks.UnlockByID(id.ID()) - if _, err := client.DeleteCertificateContacts(ctx, id.KeyVaultBaseUrl); err != nil { - return fmt.Errorf("deleting %s: %+v", id, err) + // first check to see if contacts exists or not, if they do + // delete them, else just remove them from the state file... + existing, err := client.GetCertificateContacts(ctx, id.KeyVaultBaseUrl) + + if err != nil { + // If we don't have access to the dataplane due to the public network access + // being disabled just return error and keep the state unchanged... + if utils.ResponseWasForbidden(existing.Response) { + metadata.Logger.Infof("[DELETE] 'GetCertificateContacts' error result was 'Forbidden' for key vault URL %s - dataplane access denied, keep current state and return error", id.KeyVaultBaseUrl) + return fmt.Errorf("unable to delete %s due to data plane access restrictions: %+v", id, err) + } + + // Since contacts can now be empty Not Found is no longer a reason to return an error... + if !utils.ResponseWasNotFound(existing.Response) { + // The GET call found Key Vault contacts, try to delete them... + if _, err := client.DeleteCertificateContacts(ctx, id.KeyVaultBaseUrl); err != nil { + return fmt.Errorf("deleting %s: %+v", id, err) + } + } else { + metadata.Logger.Infof("[DELETE] 'GetCertificateContacts' error result was 'NotFound' for key vault URL %s - removing from state file", id.KeyVaultBaseUrl) + return metadata.MarkAsGone(id) + } } return nil diff --git a/internal/services/keyvault/key_vault_resource.go b/internal/services/keyvault/key_vault_resource.go index 531e0bde41a3..d91f398f622b 100644 --- a/internal/services/keyvault/key_vault_resource.go +++ b/internal/services/keyvault/key_vault_resource.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" commonValidate "github.com/hashicorp/terraform-provider-azurerm/helpers/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/locks" "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/migration" "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/validate" @@ -30,13 +31,15 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" "github.com/hashicorp/terraform-provider-azurerm/internal/timeouts" "github.com/hashicorp/terraform-provider-azurerm/utils" + + // TODO: Remove in 4.0 dataplane "github.com/tombuildsstuff/kermit/sdk/keyvault/7.4/keyvault" ) var keyVaultResourceName = "azurerm_key_vault" func resourceKeyVault() *pluginsdk.Resource { - return &pluginsdk.Resource{ + resource := &pluginsdk.Resource{ Create: resourceKeyVaultCreate, Read: resourceKeyVaultRead, Update: resourceKeyVaultUpdate, @@ -202,27 +205,6 @@ func resourceKeyVault() *pluginsdk.Resource { ValidateFunc: validation.IntBetween(7, 90), }, - "contact": { - Type: pluginsdk.TypeSet, - Optional: true, - Elem: &pluginsdk.Resource{ - Schema: map[string]*pluginsdk.Schema{ - "email": { - Type: pluginsdk.TypeString, - Required: true, - }, - "name": { - Type: pluginsdk.TypeString, - Optional: true, - }, - "phone": { - Type: pluginsdk.TypeString, - Optional: true, - }, - }, - }, - }, - "tags": commonschema.Tags(), // Computed @@ -232,12 +214,41 @@ func resourceKeyVault() *pluginsdk.Resource { }, }, } + + if !features.FourPointOhBeta() { + resource.Schema["contact"] = &pluginsdk.Schema{ + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "email": { + Type: pluginsdk.TypeString, + Required: true, + }, + "name": { + Type: pluginsdk.TypeString, + Optional: true, + }, + "phone": { + Type: pluginsdk.TypeString, + Optional: true, + }, + }, + }, + Deprecated: "This property has been deprecated and will be removed in v4.0 of the provider, please use the 'azurerm_key_vault_certificate_contacts' resource to manage this property.", + } + } + + return resource } func resourceKeyVaultCreate(d *pluginsdk.ResourceData, meta interface{}) error { subscriptionId := meta.(*clients.Client).Account.SubscriptionId client := meta.(*clients.Client).KeyVault.VaultsClient + + // TODO: Remove in 4.0 dataPlaneClient := meta.(*clients.Client).KeyVault.ManagementClient + ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) defer cancel() @@ -283,9 +294,15 @@ func resourceKeyVaultCreate(d *pluginsdk.ResourceData, meta interface{}) error { } isPublic := d.Get("public_network_access_enabled").(bool) - contactRaw := d.Get("contact").(*pluginsdk.Set).List() - if !isPublic && len(contactRaw) > 0 { - return fmt.Errorf("`contact` cannot be specified when `public_network_access_enabled` is set to `false`") + + // TODO: Remove in 4.0 + contactRaw := make([]interface{}, 0) + + if !features.FourPointOhBeta() { + contactRaw = d.Get("contact").(*pluginsdk.Set).List() + if !isPublic && len(contactRaw) > 0 { + return fmt.Errorf("`contact` cannot be specified when `public_network_access_enabled` is set to `false`") + } } tenantUUID := d.Get("tenant_id").(string) @@ -413,16 +430,18 @@ func resourceKeyVaultCreate(d *pluginsdk.ResourceData, meta interface{}) error { } } - if len(contactRaw) > 0 { - if !isPublic { - return fmt.Errorf("`contact` cannot be specified when `public_network_access_enabled` is set to `false`") - } + if !features.FourPointOhBeta() { + if len(contactRaw) > 0 { + if !isPublic { + return fmt.Errorf("`contact` cannot be specified when `public_network_access_enabled` is set to `false`") + } - contacts := dataplane.Contacts{ - ContactList: expandKeyVaultCertificateContactList(contactRaw), - } - if _, err := dataPlaneClient.SetCertificateContacts(ctx, vaultUri, contacts); err != nil { - return fmt.Errorf("failed to set Contacts for %s: %+v", id, err) + contacts := dataplane.Contacts{ + ContactList: expandKeyVaultCertificateContactList(contactRaw), + } + if _, err := dataPlaneClient.SetCertificateContacts(ctx, vaultUri, contacts); err != nil { + return fmt.Errorf("failed to set Contacts for %s: %+v", id, err) + } } } @@ -431,7 +450,10 @@ func resourceKeyVaultCreate(d *pluginsdk.ResourceData, meta interface{}) error { func resourceKeyVaultUpdate(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).KeyVault.VaultsClient + + // TODO: Remove in 4.0 managementClient := meta.(*clients.Client).KeyVault.ManagementClient + ctx, cancel := timeouts.ForUpdate(meta.(*clients.Client).StopContext, d) defer cancel() @@ -566,7 +588,7 @@ func resourceKeyVaultUpdate(d *pluginsdk.ResourceData, meta interface{}) error { update.Properties = &vaults.VaultPatchProperties{} } - isPublic = d.Get("public_network_access_enabled").(bool) + isPublic := d.Get("public_network_access_enabled").(bool) if isPublic { update.Properties.PublicNetworkAccess = utils.String("Enabled") } else { @@ -623,30 +645,32 @@ func resourceKeyVaultUpdate(d *pluginsdk.ResourceData, meta interface{}) error { return fmt.Errorf("updating %s: %+v", *id, err) } - if d.HasChange("contact") { - if !isPublic { - return fmt.Errorf("`contact` cannot be specified when `public_network_access_enabled` is set to `false`") - } - contacts := dataplane.Contacts{ - ContactList: expandKeyVaultCertificateContactList(d.Get("contact").(*pluginsdk.Set).List()), - } - vaultUri := "" - if existing.Model != nil && existing.Model.Properties.VaultUri != nil { - vaultUri = *existing.Model.Properties.VaultUri - } - if vaultUri == "" { - return fmt.Errorf("failed to get vault base url for %s: %s", *id, err) - } + if !features.FourPointOhBeta() { + if d.HasChange("contact") { + if !isPublic { + return fmt.Errorf("`contact` cannot be specified when `public_network_access_enabled` is set to `false`") + } + contacts := dataplane.Contacts{ + ContactList: expandKeyVaultCertificateContactList(d.Get("contact").(*pluginsdk.Set).List()), + } + vaultUri := "" + if existing.Model != nil && existing.Model.Properties.VaultUri != nil { + vaultUri = *existing.Model.Properties.VaultUri + } + if vaultUri == "" { + return fmt.Errorf("failed to get vault base url for %s: %s", *id, err) + } - var err error - if len(*contacts.ContactList) == 0 { - _, err = managementClient.DeleteCertificateContacts(ctx, vaultUri) - } else { - _, err = managementClient.SetCertificateContacts(ctx, vaultUri, contacts) - } + var err error + if len(*contacts.ContactList) == 0 { + _, err = managementClient.DeleteCertificateContacts(ctx, vaultUri) + } else { + _, err = managementClient.SetCertificateContacts(ctx, vaultUri, contacts) + } - if err != nil { - return fmt.Errorf("setting Contacts for %s: %+v", *id, err) + if err != nil { + return fmt.Errorf("setting Contacts for %s: %+v", *id, err) + } } } @@ -657,7 +681,10 @@ func resourceKeyVaultUpdate(d *pluginsdk.ResourceData, meta interface{}) error { func resourceKeyVaultRead(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).KeyVault.VaultsClient + + // TODO: Remove in 4.0 dataplaneClient := meta.(*clients.Client).KeyVault.ManagementClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) defer cancel() @@ -690,15 +717,19 @@ func resourceKeyVaultRead(d *pluginsdk.ResourceData, meta interface{}) error { meta.(*clients.Client).KeyVault.AddToCache(*id, vaultUri) } + // TODO: Remove in 4.0 var contactsResp *dataplane.Contacts - if isPublic { - contacts, err := dataplaneClient.GetCertificateContacts(ctx, vaultUri) - if err != nil { - if !utils.ResponseWasForbidden(contacts.Response) && !utils.ResponseWasNotFound(contacts.Response) { - return fmt.Errorf("retrieving `contact` for KeyVault: %+v", err) + + if !features.FourPointOhBeta() { + if isPublic { + contacts, err := dataplaneClient.GetCertificateContacts(ctx, vaultUri) + if err != nil { + if !utils.ResponseWasForbidden(contacts.Response) && !utils.ResponseWasNotFound(contacts.Response) { + return fmt.Errorf("retrieving `contact` for KeyVault: %+v", err) + } } + contactsResp = &contacts } - contactsResp = &contacts } d.Set("name", id.VaultName) @@ -749,8 +780,10 @@ func resourceKeyVaultRead(d *pluginsdk.ResourceData, meta interface{}) error { return fmt.Errorf("setting `access_policy`: %+v", err) } - if err := d.Set("contact", flattenKeyVaultCertificateContactList(contactsResp)); err != nil { - return fmt.Errorf("setting `contact` for KeyVault: %+v", err) + if !features.FourPointOhBeta() { + if err := d.Set("contact", flattenKeyVaultCertificateContactList(contactsResp)); err != nil { + return fmt.Errorf("setting `contact` for KeyVault: %+v", err) + } } if err := tags.FlattenAndSet(d, model.Tags); err != nil { @@ -916,19 +949,22 @@ func expandKeyVaultNetworkAcls(input []interface{}) (*vaults.NetworkRuleSet, []s return &ruleSet, subnetIds } +// TODO: Remove in 4.0 func expandKeyVaultCertificateContactList(input []interface{}) *[]dataplane.Contact { results := make([]dataplane.Contact, 0) if len(input) == 0 || input[0] == nil { return &results } - for _, item := range input { - v := item.(map[string]interface{}) - results = append(results, dataplane.Contact{ - Name: utils.String(v["name"].(string)), - EmailAddress: utils.String(v["email"].(string)), - Phone: utils.String(v["phone"].(string)), - }) + if !features.FourPointOhBeta() { + for _, item := range input { + v := item.(map[string]interface{}) + results = append(results, dataplane.Contact{ + Name: utils.String(v["name"].(string)), + EmailAddress: utils.String(v["email"].(string)), + Phone: utils.String(v["phone"].(string)), + }) + } } return &results @@ -974,33 +1010,36 @@ func flattenKeyVaultNetworkAcls(input *vaults.NetworkRuleSet) []interface{} { } } +// TODO: Remove in 4.0 func flattenKeyVaultCertificateContactList(input *dataplane.Contacts) []interface{} { results := make([]interface{}, 0) if input == nil || input.ContactList == nil { return results } - for _, contact := range *input.ContactList { - emailAddress := "" - if contact.EmailAddress != nil { - emailAddress = *contact.EmailAddress - } + if !features.FourPointOhBeta() { + for _, contact := range *input.ContactList { + emailAddress := "" + if contact.EmailAddress != nil { + emailAddress = *contact.EmailAddress + } - name := "" - if contact.Name != nil { - name = *contact.Name - } + name := "" + if contact.Name != nil { + name = *contact.Name + } - phone := "" - if contact.Phone != nil { - phone = *contact.Phone - } + phone := "" + if contact.Phone != nil { + phone = *contact.Phone + } - results = append(results, map[string]interface{}{ - "email": emailAddress, - "name": name, - "phone": phone, - }) + results = append(results, map[string]interface{}{ + "email": emailAddress, + "name": name, + "phone": phone, + }) + } } return results