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_postgresql_server_key - Add CMK support #8126

Merged
merged 18 commits into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from 10 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
5 changes: 5 additions & 0 deletions azurerm/internal/services/postgres/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Client struct {
DatabasesClient *postgresql.DatabasesClient
FirewallRulesClient *postgresql.FirewallRulesClient
ServersClient *postgresql.ServersClient
ServerKeysClient *postgresql.ServerKeysClient
ServerSecurityAlertPoliciesClient *postgresql.ServerSecurityAlertPoliciesClient
VirtualNetworkRulesClient *postgresql.VirtualNetworkRulesClient
ServerAdministratorsClient *postgresql.ServerAdministratorsClient
Expand All @@ -28,6 +29,9 @@ func NewClient(o *common.ClientOptions) *Client {
serversClient := postgresql.NewServersClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&serversClient.Client, o.ResourceManagerAuthorizer)

serverKeysClient := postgresql.NewServerKeysClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&serverKeysClient.Client, o.ResourceManagerAuthorizer)

serverSecurityAlertPoliciesClient := postgresql.NewServerSecurityAlertPoliciesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&serverSecurityAlertPoliciesClient.Client, o.ResourceManagerAuthorizer)

Expand All @@ -42,6 +46,7 @@ func NewClient(o *common.ClientOptions) *Client {
DatabasesClient: &databasesClient,
FirewallRulesClient: &firewallRulesClient,
ServersClient: &serversClient,
ServerKeysClient: &serverKeysClient,
ServerSecurityAlertPoliciesClient: &serverSecurityAlertPoliciesClient,
VirtualNetworkRulesClient: &virtualNetworkRulesClient,
ServerAdministratorsClient: &serverAdministratorsClient,
Expand Down
38 changes: 38 additions & 0 deletions azurerm/internal/services/postgres/parse/server_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package parse

import (
"fmt"

"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure"
)

type PostgreSQLServerKeyId struct {
Name string
ServerName string
ResourceGroup string
}

func PostgreSQLServerKeyID(input string) (*PostgreSQLServerKeyId, error) {
id, err := azure.ParseAzureResourceID(input)
if err != nil {
return nil, fmt.Errorf("[ERROR] Unable to parse Postgres Server ID %q: %+v", input, err)
ArcturusZhang marked this conversation as resolved.
Show resolved Hide resolved
}

server := PostgreSQLServerKeyId{
ResourceGroup: id.ResourceGroup,
}

if server.ServerName, err = id.PopSegment("servers"); err != nil {
return nil, err
}

if server.Name, err = id.PopSegment("keys"); err != nil {
return nil, err
}

if err := id.ValidateNoEmptySegments(input); err != nil {
return nil, err
}

return &server, nil
}
86 changes: 86 additions & 0 deletions azurerm/internal/services/postgres/parse/server_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package parse

import "testing"

func TestPostgreSQLServerKeyID(t *testing.T) {
testData := []struct {
Name string
Input string
Expected *PostgreSQLServerKeyId
}{
{
Name: "Empty",
Input: "",
Expected: nil,
},
{
Name: "No Resource Groups Segment",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000",
Expected: nil,
},
{
Name: "No Resource Groups Value",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/",
Expected: nil,
},
{
Name: "Resource Group ID",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/",
Expected: nil,
},
{
Name: "Missing Servers Value",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.DBforPostgreSQL/servers/",
Expected: nil,
},
{
Name: "Postgres Server ID",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.DBforPostgreSQL/servers/Server1/",
Expected: nil,
},
{
Name: "Missing Key Name",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.DBforPostgreSQL/servers/Server1/keys/",
Expected: nil,
},
{
Name: "PostgreSQL Server Key ID",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.DBforPostgreSQL/servers/Server1/keys/key1",
Expected: &PostgreSQLServerKeyId{
Name: "key1",
ServerName: "Server1",
ResourceGroup: "resGroup1",
},
},
{
Name: "Wrong Casing",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.DBforPostgreSQL/Servers/",
Expected: nil,
},
}

for _, v := range testData {
t.Logf("[DEBUG] Testing %q", v.Name)

actual, err := PostgreSQLServerKeyID(v.Input)
if err != nil {
if v.Expected == nil {
continue
}

t.Fatalf("Expected a value but got an error: %s", err)
}

if actual.Name != v.Expected.Name {
t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name)
}

if actual.ServerName != v.Expected.ServerName {
t.Fatalf("Expected %q but got %q for Name", v.Expected.ServerName, actual.ServerName)
}

if actual.ResourceGroup != v.Expected.ResourceGroup {
t.Fatalf("Expected %q but got %q for Resource Group", v.Expected.ResourceGroup, actual.ResourceGroup)
}
}
}
193 changes: 193 additions & 0 deletions azurerm/internal/services/postgres/postgresql_server_key_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package postgres

import (
"context"
"fmt"
"log"
"time"

"github.com/Azure/azure-sdk-for-go/services/keyvault/mgmt/2018-02-14/keyvault"
"github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2020-01-01/postgresql"
"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/locks"
keyVaultParse "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/keyvault/parse"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/postgres/parse"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/postgres/validate"
azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

func resourceArmPostgreSQLServerKey() *schema.Resource {
return &schema.Resource{
Create: resourceArmPostgreSQLServerKeyCreateUpdate,
Read: resourceArmPostgreSQLServerKeyRead,
Update: resourceArmPostgreSQLServerKeyCreateUpdate,
Delete: resourceArmPostgreSQLServerKeyDelete,

Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error {
_, err := parse.PostgreSQLServerKeyID(id)
return err
}),

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),
},
ArcturusZhang marked this conversation as resolved.
Show resolved Hide resolved

Schema: map[string]*schema.Schema{
"server_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.PostgresServerServerID,
},

"key_vault_key_id": {
Type: schema.TypeString,
Required: true,
ValidateFunc: azure.ValidateKeyVaultChildId,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this also support the "versionless" secrets? If so we'll likely need to look up the current version to be able to use that as a name?

Copy link
Contributor Author

@ArcturusZhang ArcturusZhang Aug 25, 2020

Choose a reason for hiding this comment

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

By saying key_vault_key_id, it means some key URL WITH version like this:

https://YourVaultName.vault.azure.net/keys/YourKeyName/01234567890123456789012345678901>

Do you suggest we change this schema to align with the CMK for storage? i.e, like this:

key_vault_id = 
key_name = 
key_version = 

to emphasize that we are requiring a versioned key here?

Copy link
Contributor

Choose a reason for hiding this comment

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

right however most Azure API's which support a versioned secret also take a "versionless" version too (since the 'version' gets parsed as empty, which the Key Vault API's return as "latest") - so we should test that

No, this makes sense to leave as key_vault_key_id - however we need to confirm if this validation needs to accept both a versioned and versionless Key ID, which we'll identify through testing that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure I will have a try with this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just have a try if we pass a key URL without version, and get an internalServerError.
And per their document page, they should be expecting the key URL with version

Copy link
Collaborator

Choose a reason for hiding this comment

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

Then should we be validing that the id has a version then?

Copy link
Contributor Author

@ArcturusZhang ArcturusZhang Sep 16, 2020

Choose a reason for hiding this comment

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

This function ValidateKeyVaultChildId is expecting this ID to have a version. We have a ValidateKeyVaultChildIdVersionOptional for the version-optional scenario

},
},
}
}

func getPostgreSQLServerKeyName(ctx context.Context, vaultsClient *keyvault.VaultsClient, keyVaultKeyURI string) (string, error) {
keyVaultKeyID, err := azure.ParseKeyVaultChildID(keyVaultKeyURI)
if err != nil {
return "", err
}
keyVaultIDRaw, err := azure.GetKeyVaultIDFromBaseUrl(ctx, vaultsClient, keyVaultKeyID.KeyVaultBaseUrl)
if err != nil {
return "", err
}
keyVaultID, err := keyVaultParse.KeyVaultID(*keyVaultIDRaw)
if err != nil {
return "", err
}
return fmt.Sprintf("%s_%s_%s", keyVaultID.Name, keyVaultKeyID.Name, keyVaultKeyID.Version), nil
}

func resourceArmPostgreSQLServerKeyCreateUpdate(d *schema.ResourceData, meta interface{}) error {
keysClient := meta.(*clients.Client).Postgres.ServerKeysClient
vaultsClient := meta.(*clients.Client).KeyVault.VaultsClient
ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d)
defer cancel()

serverID, err := parse.PostgresServerServerID(d.Get("server_id").(string))
ArcturusZhang marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
keyVaultKeyURI := d.Get("key_vault_key_id").(string)
name, err := getPostgreSQLServerKeyName(ctx, vaultsClient, keyVaultKeyURI)
if err != nil {
return fmt.Errorf("cannot compose name for PostgreSQL Server Key (Resource Group %q / Server %q): %+v", serverID.ResourceGroup, serverID.Name, err)
}

locks.ByName(serverID.Name, postgreSQLServerResourceName)
defer locks.UnlockByName(serverID.Name, postgreSQLServerResourceName)

if d.IsNewResource() {
// This resource is a singleton, but its name can be anything.
// If you create a new key with different name with the old key, the service will not give you any warning but directly replace the old key with the new key.
// Therefore sometimes you cannot get the old key using the GET API since you may not know the name of the old key
resp, err := keysClient.List(ctx, serverID.ResourceGroup, serverID.Name)
if err != nil {
return fmt.Errorf("listing existing PostgreSQL Server Keys in Resource Group %q / Server %q: %+v", serverID.ResourceGroup, serverID.Name, err)
}
keys := resp.Values()
if len(keys) > 1 {
return fmt.Errorf("expecting at most one PostgreSQL Server Key, but got %q", len(keys))
}
if len(keys) == 1 && keys[0].ID != nil && *keys[0].ID != "" {
return tf.ImportAsExistsError("azurerm_postgresql_server_key", *keys[0].ID)
}
}

param := postgresql.ServerKey{
ServerKeyProperties: &postgresql.ServerKeyProperties{
ServerKeyType: utils.String("AzureKeyVault"),
URI: utils.String(d.Get("key_vault_key_id").(string)),
},
}

future, err := keysClient.CreateOrUpdate(ctx, serverID.Name, name, param, serverID.ResourceGroup)
if err != nil {
return fmt.Errorf("creating/updating PostgreSQL Server Key %q (Resource Group %q / Server %q): %+v", name, serverID.ResourceGroup, serverID.Name, err)
}
if err := future.WaitForCompletionRef(ctx, keysClient.Client); err != nil {
return fmt.Errorf("waiting for creation/update of PostgreSQL Server Key %q (Resource Group %q / Server %q): %+v", name, serverID.ResourceGroup, serverID.Name, err)
}

resp, err := keysClient.Get(ctx, serverID.ResourceGroup, serverID.Name, name)
if err != nil {
return fmt.Errorf("retrieving PostgreSQL Server Key %q (Resource Group %q / Server %q): %+v", name, serverID.ResourceGroup, serverID.Name, err)
}
if resp.ID == nil || *resp.ID == "" {
return fmt.Errorf("nil or empty ID returned for PostgreSQL Server Key %q (Resource Group %q / Server %q): %+v", name, serverID.ResourceGroup, serverID.Name, err)
}

d.SetId(*resp.ID)
return resourceArmPostgreSQLServerKeyRead(d, meta)
}

func resourceArmPostgreSQLServerKeyRead(d *schema.ResourceData, meta interface{}) error {
serversClient := meta.(*clients.Client).Postgres.ServersClient
keysClient := meta.(*clients.Client).Postgres.ServerKeysClient
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
defer cancel()

id, err := parse.PostgreSQLServerKeyID(d.Id())
if err != nil {
return err
}

resp, err := keysClient.Get(ctx, id.ResourceGroup, id.ServerName, id.Name)
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
log.Printf("[WARN] PostgreSQL Server Key %q was not found (Resource Group %q / Server %q)", id.Name, id.ResourceGroup, id.ServerName)
d.SetId("")
return nil
}

return fmt.Errorf("retrieving PostgreSQL Server Key %q (Resource Group %q / Server %q): %+v", id.Name, id.ResourceGroup, id.ServerName, err)
}

respServer, err := serversClient.Get(ctx, id.ResourceGroup, id.ServerName)
if err != nil {
return fmt.Errorf("cannot get MySQL Server ID: %+v", err)
}

d.Set("server_id", respServer.ID)
if props := resp.ServerKeyProperties; props != nil {
d.Set("key_vault_key_id", props.URI)
}

return nil
}

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

id, err := parse.PostgreSQLServerKeyID(d.Id())
if err != nil {
return err
}

future, err := client.Delete(ctx, id.ServerName, id.Name, id.ResourceGroup)
ArcturusZhang marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("deleting PostgreSQL Server Key %q (Resource Group %q / Server %q): %+v", id.Name, id.ResourceGroup, id.ServerName, err)
}
if err := future.WaitForCompletionRef(ctx, client.Client); err != nil {
return fmt.Errorf("waiting for deletion of PostgreSQL Server Key %q (Resource Group %q / Server %q): %+v", id.Name, id.ResourceGroup, id.ServerName, err)
}

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import (
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

const (
postgreSQLServerResourceName = "azurerm_postgresql_server"
)

func resourceArmPostgreSQLServer() *schema.Resource {
return &schema.Resource{
Create: resourceArmPostgreSQLServerCreate,
Expand Down
1 change: 1 addition & 0 deletions azurerm/internal/services/postgres/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource {
"azurerm_postgresql_database": resourceArmPostgreSQLDatabase(),
"azurerm_postgresql_firewall_rule": resourceArmPostgreSQLFirewallRule(),
"azurerm_postgresql_server": resourceArmPostgreSQLServer(),
"azurerm_postgresql_server_key": resourceArmPostgreSQLServerKey(),
"azurerm_postgresql_virtual_network_rule": resourceArmPostgreSQLVirtualNetworkRule(),
"azurerm_postgresql_active_directory_administrator": resourceArmPostgreSQLAdministrator(),
}
Expand Down
Loading