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_mysql_server_key - Add CMK support #8125

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

ServerKeysClient := mysql.NewServerKeysClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&ServerKeysClient.Client, o.ResourceManagerAuthorizer)

serverSecurityAlertPoliciesClient := mysql.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
194 changes: 194 additions & 0 deletions azurerm/internal/services/mysql/mysql_server_key_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package mysql

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/mysql/mgmt/2020-01-01/mysql"
"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/mysql/parse"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mysql/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 resourceArmMySQLServerKey() *schema.Resource {
return &schema.Resource{
Create: resourceArmMySQLServerKeyCreateUpdate,
Read: resourceArmMySQLServerKeyRead,
Update: resourceArmMySQLServerKeyCreateUpdate,
Delete: resourceArmMySQLServerKeyDelete,

Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error {
_, err := parse.MySQLServerKeyID(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.MysqlServerServerID,
},

"key_vault_key_id": {
Type: schema.TypeString,
Required: true,
ValidateFunc: azure.ValidateKeyVaultChildId,
katbyte marked this conversation as resolved.
Show resolved Hide resolved
},
},
}
}

func getMySQLServerKeyName(ctx context.Context, vaultsClient *keyvault.VaultsClient, keyVaultKeyURI string) (string, error) {
ArcturusZhang marked this conversation as resolved.
Show resolved Hide resolved
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 resourceArmMySQLServerKeyCreateUpdate(d *schema.ResourceData, meta interface{}) error {
keysClient := meta.(*clients.Client).MySQL.ServerKeysClient
vaultsClient := meta.(*clients.Client).KeyVault.VaultsClient
ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d)
defer cancel()

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

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

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 MySQL 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 MySQL Server Key, but got %q", len(keys))
}
if len(keys) == 1 && keys[0].ID != nil && *keys[0].ID != "" {
return tf.ImportAsExistsError("azurerm_mysql_server_key", *keys[0].ID)
}
}

param := mysql.ServerKey{
ServerKeyProperties: &mysql.ServerKeyProperties{
ServerKeyType: utils.String("AzureKeyVault"),
URI: &keyVaultKeyURI,
},
}

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

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

d.SetId(*resp.ID)

return resourceArmMySQLServerKeyRead(d, meta)
}

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

id, err := parse.MySQLServerKeyID(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] MySQL 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 MySQL 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 resourceArmMySQLServerKeyDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).MySQL.ServerKeysClient
ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d)
defer cancel()

id, err := parse.MySQLServerKeyID(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 MySQL 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 MySQL Server Key %q (Resource Group %q / Server %q): %+v", id.Name, id.ResourceGroup, id.ServerName, err)
}

return nil
}
4 changes: 4 additions & 0 deletions azurerm/internal/services/mysql/mysql_server_resource.go
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 (
mySQLServerResourceName = "azurerm_mysql_server"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

presumably we'll also need to lock in the create/update/delete here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I assume we do not need locks in postgresql server.

This key resource has a kind of weird behaviour - it is a separated resource from the server by service design, but it is a singleton, you could only have one key at one time. If you are trying to create two keys (with different name) for one server, after the second key is created on the service side, the first key will be automatically removed without any notice.

Therefore we need locks in the server key resource to ensure that if the user is not using this resource correctly (by say create two keys for one server), the order of key creation is predictable - this will always give the user a diff after apply.

The server does not have this kind of constraint, therefore I suppose the server resource does not need locks.

Copy link
Contributor

@tombuildsstuff tombuildsstuff Aug 25, 2020

Choose a reason for hiding this comment

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

so what happens if you try and decrypt the server and delete it simultaneously? a conflict will likely occur somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By decrypting the server, I assume we are deleting the server key - since we locked the server in the delete function of server key resource, we could not decrypt the server and delete it simultaneously - during the decrypting, the server is locked and deletion should hold on.

Copy link
Contributor

Choose a reason for hiding this comment

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

can we test this to confirm?

Copy link
Contributor

Choose a reason for hiding this comment

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

@ArcturusZhang any update here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tests have been done per requested.


func resourceArmMySqlServer() *schema.Resource {
return &schema.Resource{
Create: resourceArmMySqlServerCreate,
Expand Down
37 changes: 37 additions & 0 deletions azurerm/internal/services/mysql/parse/mysql_server_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package parse

import (
"fmt"

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

type MySQLServerKeyId struct {
ResourceGroup string
ServerName string
Name string
}

func MySQLServerKeyID(input string) (*MySQLServerKeyId, error) {
id, err := azure.ParseAzureResourceID(input)
if err != nil {
return nil, fmt.Errorf("unable to parse MySQL Server Key ID %q: %+v", input, err)
}

key := MySQLServerKeyId{
ResourceGroup: id.ResourceGroup,
}

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

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

return &key, nil
}
1 change: 1 addition & 0 deletions azurerm/internal/services/mysql/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource {
"azurerm_mysql_database": resourceArmMySqlDatabase(),
"azurerm_mysql_firewall_rule": resourceArmMySqlFirewallRule(),
"azurerm_mysql_server": resourceArmMySqlServer(),
"azurerm_mysql_server_key": resourceArmMySQLServerKey(),
"azurerm_mysql_virtual_network_rule": resourceArmMySSQLVirtualNetworkRule(),
"azurerm_mysql_active_directory_administrator": resourceArmMySQLAdministrator()}
}
Loading