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 Data Source: azurerm_key_vault_secret_versions #27393

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/labeler-issue-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ service/iot-hub:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_iothub((.|\n)*)###'

service/key-vault:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(key_vault\W+|key_vault_access_policy\W+|key_vault_certificate\W+|key_vault_certificate_contacts\W+|key_vault_certificate_data\W+|key_vault_certificate_issuer\W+|key_vault_certificates\W+|key_vault_encrypted_value\W+|key_vault_key\W+|key_vault_managed_storage_account\W+|key_vault_managed_storage_account_sas_token_definition\W+|key_vault_secret\W+|key_vault_secrets\W+)((.|\n)*)###'
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(key_vault\W+|key_vault_access_policy\W+|key_vault_certificate\W+|key_vault_certificate_contacts\W+|key_vault_certificate_data\W+|key_vault_certificate_issuer\W+|key_vault_certificates\W+|key_vault_encrypted_value\W+|key_vault_key\W+|key_vault_managed_storage_account\W+|key_vault_managed_storage_account_sas_token_definition\W+|key_vault_secret\W+|key_vault_secret_versions\W+|key_vault_secrets\W+)((.|\n)*)###'

service/kusto:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_kusto_((.|\n)*)###'
Expand Down
216 changes: 216 additions & 0 deletions internal/services/keyvault/key_vault_secret_versions_data_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package keyvault

import (
"context"
"fmt"
"sort"
"strings"
"time"

"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
keyVaultValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/validate"
"github.com/tombuildsstuff/kermit/sdk/keyvault/7.4/keyvault"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should reference kermit from jackofallops

Suggested change
"github.com/tombuildsstuff/kermit/sdk/keyvault/7.4/keyvault"
"github.com/jackofallops/kermit/sdk/keyvault/7.4/keyvault"

)

var _ sdk.DataSource = KeyVaultSecretVersionsDataSource{}

type KeyVaultSecretVersionsDataSource struct{}

type KeyVaultSecretVersionsDataSourceModel struct {
Name string `tfschema:"name"`
KeyVaultId string `tfschema:"key_vault_id"`
MaxResults int64 `tfschema:"max_results"`
Versions []secretVersionModel `tfschema:"versions"`
}

type secretVersionModel struct {
ID string `tfschema:"id"`
CreatedDate string `tfschema:"created_date"`
Enabled bool `tfschema:"enabled"`
NotBeforeDate string `tfschema:"not_before_date"`
ExpirationDate string `tfschema:"expiration_date"`
UpdatedDate string `tfschema:"updated_date"`
Uri string `tfschema:"uri"`
}

func (r KeyVaultSecretVersionsDataSource) Arguments() map[string]*schema.Schema {
return map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ValidateFunc: keyVaultValidate.NestedItemName,
},

"key_vault_id": {
Type: schema.TypeString,
Required: true,
ValidateFunc: commonids.ValidateKeyVaultID,
},

"max_results": {
Type: schema.TypeInt,
Optional: true,
Default: 25,
},
}
}

func (r KeyVaultSecretVersionsDataSource) Attributes() map[string]*schema.Schema {
return map[string]*schema.Schema{
"versions": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
},

"created_date": {
Type: schema.TypeString,
Computed: true,
},

"enabled": {
Type: schema.TypeBool,
Computed: true,
},

"not_before_date": {
Type: schema.TypeString,
Computed: true,
},

"expiration_date": {
Type: schema.TypeString,
Computed: true,
},

"updated_date": {
Type: schema.TypeString,
Computed: true,
},
"uri": {
Type: schema.TypeString,
Computed: true,
},
},
},
},
}
}

func (r KeyVaultSecretVersionsDataSource) ResourceType() string {
return "azurerm_key_vault_secret_versions"
}

func (r KeyVaultSecretVersionsDataSource) ModelObject() interface{} {
return &KeyVaultSecretVersionsDataSourceModel{}
}

func (r KeyVaultSecretVersionsDataSource) Read() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 5 * time.Minute,

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.KeyVault.ManagementClient

var model KeyVaultSecretVersionsDataSourceModel

if err := metadata.Decode(&model); err != nil {
return fmt.Errorf("decoding: %+v", err)
}

keyVaultId, err := commonids.ParseKeyVaultID(model.KeyVaultId)
if err != nil {
return err
}

keyVaultUri, err := metadata.Client.KeyVault.BaseUriForKeyVault(ctx, *keyVaultId)
if err != nil {
return err
}

maxResults32 := int32(model.MaxResults)

resp, err := client.GetSecretVersions(ctx, *keyVaultUri, model.Name, &maxResults32)
Comment on lines +140 to +142
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this variable is only referenced once we would prefer to use the pointer.To function to pass the casted results directly into the function call

Suggested change
maxResults32 := int32(model.MaxResults)
resp, err := client.GetSecretVersions(ctx, *keyVaultUri, model.Name, &maxResults32)
resp, err := client.GetSecretVersions(ctx, *keyVaultUri, model.Name, pointer.To(int32(model.MaxResults)))

if err != nil {
return fmt.Errorf("making List Versions request on Azure KeyVault Secret %s: %+v", model.Name, err)
}

if resp.Values() != nil {
for resp.NotDone() {
for _, v := range resp.Values() {
model.Versions = append(model.Versions, expandSecretVersion(&v))
}
err = resp.NextWithContext(ctx)
if err != nil {
return fmt.Errorf("iterating over Secret Versions: %+v", err)
}
}

var errors []error
model.Versions, errors = sortSecretVersions(model.Versions)
if len(errors) > 0 {
return fmt.Errorf("sorting Secret Versions: %+v", errors)
}
}

metadata.ResourceData.SetId(fmt.Sprintf("%s/%s", model.KeyVaultId, model.Name))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can generate the ID using existing functions in the keyvault package, we can then use the SetID method on metadata

Suggested change
metadata.ResourceData.SetId(fmt.Sprintf("%s/%s", model.KeyVaultId, model.Name))
id := keyVaultParse.NewSecretVersionlessID(keyVaultId.SubscriptionId, keyVaultId.ResourceGroupName, keyVaultId.VaultName, model.Name)
metadata.SetID(id)


return metadata.Encode(&model)
},
}
}

func expandSecretVersion(v *keyvault.SecretItem) secretVersionModel {
var item secretVersionModel
item.Uri = *v.ID
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I understand it's highly unlikely for keyvault that these fields are not returned or empty, but we can still use pointer.From to do a nil check on this for us to prevent any potential crashes

Suggested change
item.Uri = *v.ID
item.Uri = pointer.From(v.ID)

item.ID = (*v.ID)[strings.LastIndex(*v.ID, "/")+1:]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be good to only do this transformation if v.ID is not nil or an empty string to prevent a potential panic

item.CreatedDate = time.Time(*v.Attributes.Created).Format(time.RFC3339)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're nil checking the other dates/times below before casting, I think we should do that here as well

Suggested change
item.CreatedDate = time.Time(*v.Attributes.Created).Format(time.RFC3339)
createdDate := v.Attributes.Created; createdDate != nil {
item.CreatedDate = time.Time(*createdDate).Format(time.RFC3339)
}

item.Enabled = *v.Attributes.Enabled
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for this

Suggested change
item.Enabled = *v.Attributes.Enabled
item.Enabled = pointer.From(v.Attributes.Enabled)

if notBefore := v.Attributes.NotBefore; notBefore != nil {
item.NotBeforeDate = time.Time(*notBefore).Format(time.RFC3339)
}
if expires := v.Attributes.Expires; expires != nil {
item.ExpirationDate = time.Time(*expires).Format(time.RFC3339)
}
if updated := v.Attributes.Updated; updated != nil {
item.UpdatedDate = time.Time(*updated).Format(time.RFC3339)
}

return item
}

func sortSecretVersions(values []secretVersionModel) ([]secretVersionModel, []error) {
errors := make([]error, 0)
sort.Slice(values, func(i, j int) bool {
// Sort by CreatedDate in descending order
timeA, err := time.Parse(time.RFC3339, values[i].CreatedDate)
if err != nil {
errors = append(errors, err)
return false
}

timeB, err := time.Parse(time.RFC3339, values[j].CreatedDate)
if err != nil {
errors = append(errors, err)
return false
}

return timeA.After(timeB)

})

if len(errors) > 0 {
return values, errors
}

return values, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package keyvault_test

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance"
"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check"
)

type KeyVaultSecretVersionsDataSource struct{}

func TestAccDataSourceKeyVaultSecretVersions_basic(t *testing.T) {
data := acceptance.BuildTestData(t, "data.azurerm_key_vault_secret_versions", "test")
r := KeyVaultSecretVersionsDataSource{}

data.DataSourceTest(t, []acceptance.TestStep{
{
Config: r.basic(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).Key("versions.#").HasValue("1"),
check.That(data.ResourceName).Key("versions.0.enabled").HasValue("true"),
check.That(data.ResourceName).Key("versions.0.created_date").MatchesRegex(regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$`)),
check.That(data.ResourceName).Key("versions.0.id").MatchesRegex(regexp.MustCompile(`^\w+$`)),
),
},
})
}

func TestAccDataSourceKeyVaultSecretVersions_complete(t *testing.T) {
data := acceptance.BuildTestData(t, "data.azurerm_key_vault_secret_versions", "test")
r := KeyVaultSecretVersionsDataSource{}

data.DataSourceTest(t, []acceptance.TestStep{
{
Config: r.complete(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).Key("versions.#").HasValue("1"),
check.That(data.ResourceName).Key("versions.0.enabled").HasValue("true"),
check.That(data.ResourceName).Key("versions.0.created_date").MatchesRegex(regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$`)),
check.That(data.ResourceName).Key("versions.0.not_before_date").HasValue("2019-01-01T01:02:03Z"),
check.That(data.ResourceName).Key("versions.0.expiration_date").HasValue("2020-01-01T01:02:03Z"),
check.That(data.ResourceName).Key("versions.0.updated_date").MatchesRegex(regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$`)),
check.That(data.ResourceName).Key("versions.0.id").MatchesRegex(regexp.MustCompile(`^\w+$`)),
check.That(data.ResourceName).Key("versions.0.uri").MatchesRegex(regexp.MustCompile(`^https://acctestkv-\w+.vault.azure.net/secrets/secret-\w+/\w+$`)),
),
},
})
}

func (KeyVaultSecretVersionsDataSource) basic(data acceptance.TestData) string {
return fmt.Sprintf(`
%s

data "azurerm_key_vault_secret_versions" "test" {
name = azurerm_key_vault_secret.test.name
key_vault_id = azurerm_key_vault.test.id
}
`, KeyVaultSecretResource{}.basic(data))
}

func (KeyVaultSecretVersionsDataSource) complete(data acceptance.TestData) string {
return fmt.Sprintf(`
%s

data "azurerm_key_vault_secret_versions" "test" {
name = azurerm_key_vault_secret.test.name
key_vault_id = azurerm_key_vault.test.id
}
`, KeyVaultSecretResource{}.complete(data))
}
1 change: 1 addition & 0 deletions internal/services/keyvault/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func (r Registration) SupportedResources() map[string]*pluginsdk.Resource {
func (r Registration) DataSources() []sdk.DataSource {
return []sdk.DataSource{
EncryptedValueDataSource{},
KeyVaultSecretVersionsDataSource{},
}
}

Expand Down
73 changes: 73 additions & 0 deletions website/docs/d/key_vault_secret_versions.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
subcategory: "Key Vault"
layout: "azurerm"
page_title: "Azure Resource Manager: Data Source: azurerm_key_vault_secret_versions"
description: |-
Get a list of versions for an existing Key Vault Secret.
---

# Data Source: azurerm_key_vault_secret_versions

Use this data source to access information about an existing Key Vault Secret's versions. The secret version values is not included. The `key_vault_secret` data source can be used to retrieve the value of a given secret version using it's `id`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the introduction of ephemeral resources we might want to point users towards the ephemeral resource instead of the data source

Suggested change
Use this data source to access information about an existing Key Vault Secret's versions. The secret version values is not included. The `key_vault_secret` data source can be used to retrieve the value of a given secret version using it's `id`.
Use this data source to access information about an existing Key Vault Secret's versions. The secret version values are not included. The `azurerm_key_vault_secret` ephemeral resource can be used to retrieve the value of a given secret version using it's `id` without storing the information in state. Alternatively the `azurerm_key_vault_secret` data source can be used to retrieve this information, but will store this information in state.


## Example Usage

```hcl
data "azurerm_key_vault" "example" {
name = "mykeyvault"
resource_group_name = "some-resource-group"
}

data "azurerm_key_vault_secret_versions" "example" {
name = "mysecret"
key_vault_id = data.azurerm_key_vault.example.id
}

output "versions" {
value = data.azurerm_key_vault_secret_versions.example.versions
}
```

## Arguments Reference

The following arguments are supported:

* `key_vault_id` - (Required) The ID of the Key Vault containing the secret.

* `name` - (Required) The name of the Key Vault Secret to retrieve versions from.

---

* `max_results` - (Optional) Maximum number of versions to retrieve. Defaults to `25`.
Comment on lines +37 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A space here is sufficient

Suggested change
* `name` - (Required) The name of the Key Vault Secret to retrieve versions from.
---
* `max_results` - (Optional) Maximum number of versions to retrieve. Defaults to `25`.
* `name` - (Required) The name of the Key Vault Secret to retrieve versions from.
* `max_results` - (Optional) Maximum number of versions to retrieve. Defaults to `25`.


## Attributes Reference

In addition to the Arguments listed above - the following Attributes are exported:

* `id` - The ID of the Key Vault Secret.

* `versions` - A `versions` list as defined below. The list of versions are sorted by `created_date` descending, meaning the most recently created secret version will be first in the list.

---

The `versions` entries in the list export the following:

* `created_date` - The date and time when the Key Vault Secret version was created.

* `enabled` - Is the version enabled? Returns a `bool` value.

* `expiration_date` - The date and time at which the Key Vault Secret version expires and is no longer valid.

* `id` - The Key Vault Secret version ID.

* `not_before_date` - The earliest date and time at which the Key Vault Secret version can be used.

* `updated_date` - The date and time when the Key Vault Secret version was last updated.

* `uri` - The full URI of the secret version.

## Timeouts

The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions:

* `read` - (Defaults to 5 minutes) Used when retrieving the Key Vault Secret Versions.
Loading