Skip to content

Commit

Permalink
Merge pull request #6716 from terraform-providers/f/kv-soft-delete-ch…
Browse files Browse the repository at this point in the history
…ild-recovery
  • Loading branch information
jackofallops committed May 1, 2020
2 parents 57c73af + 54df90c commit e937dc6
Show file tree
Hide file tree
Showing 9 changed files with 462 additions and 21 deletions.
Expand Up @@ -383,8 +383,32 @@ func resourceArmKeyVaultCertificateCreate(d *schema.ResourceData, meta interface
CertificatePolicy: &policy,
Tags: tags.Expand(t),
}
if _, err := client.CreateCertificate(ctx, keyVaultBaseUrl, name, parameters); err != nil {
return err
if resp, err := client.CreateCertificate(ctx, keyVaultBaseUrl, name, parameters); err != nil {
if meta.(*clients.Client).Features.KeyVault.RecoverSoftDeletedKeyVaults && utils.ResponseWasConflict(resp.Response) {
recoveredCertificate, err := client.RecoverDeletedCertificate(ctx, keyVaultBaseUrl, name)
if err != nil {
return err
}
log.Printf("[DEBUG] Recovering Secret %q with ID: %q", name, *recoveredCertificate.ID)
if certificate := recoveredCertificate.ID; certificate != nil {
stateConf := &resource.StateChangeConf{
Pending: []string{"pending"},
Target: []string{"available"},
Refresh: keyVaultChildItemRefreshFunc(*certificate),
Delay: 30 * time.Second,
PollInterval: 10 * time.Second,
ContinuousTargetOccurence: 10,
Timeout: d.Timeout(schema.TimeoutCreate),
}

if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf("Error waiting for Key Vault Secret %q to become available: %s", name, err)
}
log.Printf("[DEBUG] Secret %q recovered with ID: %q", name, *recoveredCertificate.ID)
}
} else {
return err
}
}

log.Printf("[DEBUG] Waiting for Key Vault Certificate %q in Vault %q to be provisioned", name, keyVaultBaseUrl)
Expand Down
29 changes: 27 additions & 2 deletions azurerm/internal/services/keyvault/resource_arm_key_vault_key.go
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault"
"github.com/Azure/go-autorest/autorest/date"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure"
Expand Down Expand Up @@ -221,8 +222,32 @@ func resourceArmKeyVaultKeyCreate(d *schema.ResourceData, meta interface{}) erro
parameters.KeyAttributes.Expires = &expirationUnixTime
}

if _, err := client.CreateKey(ctx, keyVaultBaseUri, name, parameters); err != nil {
return fmt.Errorf("Error Creating Key: %+v", err)
if resp, err := client.CreateKey(ctx, keyVaultBaseUri, name, parameters); err != nil {
if meta.(*clients.Client).Features.KeyVault.RecoverSoftDeletedKeyVaults && utils.ResponseWasConflict(resp.Response) {
recoveredKey, err := client.RecoverDeletedKey(ctx, keyVaultBaseUri, name)
if err != nil {
return err
}
log.Printf("[DEBUG] Recovering Key %q with ID: %q", name, *recoveredKey.Key.Kid)
if kid := recoveredKey.Key.Kid; kid != nil {
stateConf := &resource.StateChangeConf{
Pending: []string{"pending"},
Target: []string{"available"},
Refresh: keyVaultChildItemRefreshFunc(*kid),
Delay: 30 * time.Second,
PollInterval: 10 * time.Second,
ContinuousTargetOccurence: 10,
Timeout: d.Timeout(schema.TimeoutCreate),
}

if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf("Error waiting for Key Vault Secret %q to become available: %s", name, err)
}
log.Printf("[DEBUG] Key %q recovered with ID: %q", name, *kid)
}
} else {
return fmt.Errorf("Error Creating Key: %+v", err)
}
}

// "" indicates the latest version
Expand Down
75 changes: 63 additions & 12 deletions azurerm/internal/services/keyvault/resource_arm_key_vault_secret.go
Expand Up @@ -3,16 +3,17 @@ package keyvault
import (
"fmt"
"log"
"net/http"
"time"

"github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault"
"github.com/Azure/go-autorest/autorest/date"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
"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/features"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
Expand Down Expand Up @@ -99,17 +100,15 @@ func resourceArmKeyVaultSecretCreate(d *schema.ResourceData, meta interface{}) e
return fmt.Errorf("Error looking up Secret %q vault url from id %q: %+v", name, keyVaultId, err)
}

if features.ShouldResourcesBeImported() {
existing, err := client.GetSecret(ctx, keyVaultBaseUrl, name, "")
if err != nil {
if !utils.ResponseWasNotFound(existing.Response) {
return fmt.Errorf("Error checking for presence of existing Secret %q (Key Vault %q): %s", name, keyVaultBaseUrl, err)
}
existing, err := client.GetSecret(ctx, keyVaultBaseUrl, name, "")
if err != nil {
if !utils.ResponseWasNotFound(existing.Response) {
return fmt.Errorf("Error checking for presence of existing Secret %q (Key Vault %q): %s", name, keyVaultBaseUrl, err)
}
}

if existing.ID != nil && *existing.ID != "" {
return tf.ImportAsExistsError("azurerm_key_vault_secret", *existing.ID)
}
if existing.ID != nil && *existing.ID != "" {
return tf.ImportAsExistsError("azurerm_key_vault_secret", *existing.ID)
}

value := d.Get("value").(string)
Expand All @@ -135,15 +134,44 @@ func resourceArmKeyVaultSecretCreate(d *schema.ResourceData, meta interface{}) e
parameters.SecretAttributes.Expires = &expirationUnixTime
}

if _, err := client.SetSecret(ctx, keyVaultBaseUrl, name, parameters); err != nil {
return err
if resp, err := client.SetSecret(ctx, keyVaultBaseUrl, name, parameters); err != nil {
// In the case that the Secret already exists in a Soft Deleted / Recoverable state we check if `recover_soft_deleted_key_vaults` is set
// and attempt recovery where appropriate
if meta.(*clients.Client).Features.KeyVault.RecoverSoftDeletedKeyVaults && utils.ResponseWasConflict(resp.Response) {
recoveredSecret, err := client.RecoverDeletedSecret(ctx, keyVaultBaseUrl, name)
if err != nil {
return err
}
log.Printf("[DEBUG] Recovering Secret %q with ID: %q", name, *recoveredSecret.ID)
// We need to wait for consistency, recovered Key Vault Child items are not as readily available as newly created
if secret := recoveredSecret.ID; secret != nil {
stateConf := &resource.StateChangeConf{
Pending: []string{"pending"},
Target: []string{"available"},
Refresh: keyVaultChildItemRefreshFunc(*secret),
Delay: 30 * time.Second,
PollInterval: 10 * time.Second,
ContinuousTargetOccurence: 10,
Timeout: d.Timeout(schema.TimeoutCreate),
}

if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf("Error waiting for Key Vault Secret %q to become available: %s", name, err)
}
log.Printf("[DEBUG] Secret %q recovered with ID: %q", name, *recoveredSecret.ID)
}
} else {
// If the error response was anything else, or `recover_soft_deleted_key_vaults` is `false` just return the error
return err
}
}

// "" indicates the latest version
read, err := client.GetSecret(ctx, keyVaultBaseUrl, name, "")
if err != nil {
return err
}

if read.ID == nil {
return fmt.Errorf("Cannot read KeyVault Secret '%s' (in key vault '%s')", name, keyVaultBaseUrl)
}
Expand Down Expand Up @@ -339,3 +367,26 @@ func resourceArmKeyVaultSecretDelete(d *schema.ResourceData, meta interface{}) e
_, err = client.DeleteSecret(ctx, id.KeyVaultBaseUrl, id.Name)
return err
}

func keyVaultChildItemRefreshFunc(secretUri string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
log.Printf("[DEBUG] Checking to see if KeyVault Secret %q is available..", secretUri)

var PTransport = &http.Transport{Proxy: http.ProxyFromEnvironment}

client := &http.Client{
Transport: PTransport,
}

conn, err := client.Get(secretUri)
if err != nil {
log.Printf("[DEBUG] Didn't find KeyVault secret at %q", secretUri)
return nil, "pending", fmt.Errorf("Error checking secret at %q: %s", secretUri, err)
}

defer conn.Body.Close()

log.Printf("[DEBUG] Found KeyVault Secret %q", secretUri)
return "available", "available", nil
}
}
Expand Up @@ -118,6 +118,38 @@ func TestAccAzureRMKeyVaultCertificate_basicGenerate(t *testing.T) {
})
}

func TestAccAzureRMKeyVaultCertificate_softDeleteRecovery(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_key_vault_certificate", "test")

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acceptance.PreCheck(t) },
Providers: acceptance.SupportedProviders,
CheckDestroy: testCheckAzureRMKeyVaultCertificateDestroy,
Steps: []resource.TestStep{
{
Config: testAccAzureRMKeyVaultCertificate_softDeleteRecovery(data, false),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMKeyVaultCertificateExists(data.ResourceName),
resource.TestCheckResourceAttrSet(data.ResourceName, "secret_id"),
resource.TestCheckResourceAttrSet(data.ResourceName, "certificate_data"),
),
},
{
Config: testAccAzureRMKeyVaultCertificate_softDeleteRecovery(data, false),
Destroy: true,
},
{
Config: testAccAzureRMKeyVaultCertificate_softDeleteRecovery(data, true),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMKeyVaultCertificateExists(data.ResourceName),
resource.TestCheckResourceAttrSet(data.ResourceName, "secret_id"),
resource.TestCheckResourceAttrSet(data.ResourceName, "certificate_data"),
),
},
},
})
}

func TestAccAzureRMKeyVaultCertificate_basicGenerateSans(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_key_vault_certificate", "test")

Expand Down Expand Up @@ -922,3 +954,105 @@ resource "azurerm_key_vault_certificate" "test" {
}
`, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomString)
}

func testAccAzureRMKeyVaultCertificate_softDeleteRecovery(data acceptance.TestData, purge bool) string {
return fmt.Sprintf(`
provider "azurerm" {
features {
key_vault {
purge_soft_delete_on_destroy = "%t"
recover_soft_deleted_key_vaults = true
}
}
}
data "azurerm_client_config" "current" {
}
resource "azurerm_resource_group" "test" {
name = "acctestRG-kvc-%d"
location = "%s"
}
resource "azurerm_key_vault" "test" {
name = "acctestkeyvault%s"
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name
tenant_id = data.azurerm_client_config.current.tenant_id
soft_delete_enabled = true
sku_name = "standard"
access_policy {
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = data.azurerm_client_config.current.object_id
certificate_permissions = [
"create",
"delete",
"get",
"recover",
"update",
]
key_permissions = [
"create",
]
secret_permissions = [
"set",
]
storage_permissions = [
"set",
]
}
}
resource "azurerm_key_vault_certificate" "test" {
name = "acctestcert%s"
key_vault_id = azurerm_key_vault.test.id
certificate_policy {
issuer_parameters {
name = "Self"
}
key_properties {
exportable = true
key_size = 2048
key_type = "RSA"
reuse_key = true
}
lifetime_action {
action {
action_type = "AutoRenew"
}
trigger {
days_before_expiry = 30
}
}
secret_properties {
content_type = "application/x-pkcs12"
}
x509_certificate_properties {
key_usage = [
"cRLSign",
"dataEncipherment",
"digitalSignature",
"keyAgreement",
"keyCertSign",
"keyEncipherment",
]
subject = "CN=hello-world"
validity_in_months = 12
}
}
}
`, purge, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomString)
}

0 comments on commit e937dc6

Please sign in to comment.