diff --git a/docs/data-sources/cluster.md b/docs/data-sources/cluster.md index 4897ddb..66dba42 100644 --- a/docs/data-sources/cluster.md +++ b/docs/data-sources/cluster.md @@ -94,6 +94,7 @@ Read-Only: Read-Only: - `aws_cmk_spec` (Attributes) AWS CMK Provider Configuration. (see [below for nested schema](#nestedatt--cmk_spec--aws_cmk_spec)) +- `azure_cmk_spec` (Attributes) AZURE CMK Provider Configuration. (see [below for nested schema](#nestedatt--cmk_spec--azure_cmk_spec)) - `gcp_cmk_spec` (Attributes) GCP CMK Provider Configuration. (see [below for nested schema](#nestedatt--cmk_spec--gcp_cmk_spec)) - `is_enabled` (Boolean) Is Enabled - `provider_type` (String) CMK Provider Type. @@ -108,6 +109,18 @@ Read-Only: - `secret_key` (String) Secret Key + +### Nested Schema for `cmk_spec.azure_cmk_spec` + +Read-Only: + +- `client_id` (String) Client ID +- `client_secret` (String) Client Secret +- `key_name` (String) Key Name +- `key_vault_uri` (String) Key Vault URI +- `tenant_id` (String) Tenant ID + + ### Nested Schema for `cmk_spec.gcp_cmk_spec` diff --git a/docs/resources/cluster.md b/docs/resources/cluster.md index 197d19e..8b41bf8 100644 --- a/docs/resources/cluster.md +++ b/docs/resources/cluster.md @@ -364,6 +364,7 @@ To create an AWS Cluster with Customer Managed Keys ```terraform # EAR enabled single region cluster # The same cmk_spec can be used for multi region/read replica clusters as well +# Encryption at rest is supported on clusters with database version 2.16.7.0 or later variable "ysql_password" { type = string @@ -423,6 +424,7 @@ To create a GCP Cluster with Customer Managed Keys ```terraform # EAR enabled single region cluster # The same cmk_spec can be used for multi region/read replica clusters as well +# Encryption at rest is supported on clusters with database version 2.16.7.0 or later variable "ysql_password" { type = string @@ -696,6 +698,7 @@ Required: Optional: - `aws_cmk_spec` (Attributes) AWS CMK Provider Configuration. (see [below for nested schema](#nestedatt--cmk_spec--aws_cmk_spec)) +- `azure_cmk_spec` (Attributes) AZURE CMK Provider Configuration. (see [below for nested schema](#nestedatt--cmk_spec--azure_cmk_spec)) - `gcp_cmk_spec` (Attributes) GCP CMK Provider Configuration. (see [below for nested schema](#nestedatt--cmk_spec--gcp_cmk_spec)) @@ -708,6 +711,18 @@ Required: - `secret_key` (String) Secret Key + +### Nested Schema for `cmk_spec.azure_cmk_spec` + +Required: + +- `client_id` (String) Azure Active Directory (AD) Client ID for Key Vault service principal. +- `client_secret` (String) Azure AD Client Secret for Key Vault service principal. +- `key_name` (String) Name of cryptographic key in Azure Key Vault. +- `key_vault_uri` (String) URI of Azure Key Vault storing cryptographic keys. +- `tenant_id` (String) Azure AD Tenant ID for Key Vault service principal. + + ### Nested Schema for `cmk_spec.gcp_cmk_spec` diff --git a/examples/resources/ybm_cluster/single-region-aws-cmk.tf b/examples/resources/ybm_cluster/single-region-aws-cmk.tf index 34683d2..9f6af82 100644 --- a/examples/resources/ybm_cluster/single-region-aws-cmk.tf +++ b/examples/resources/ybm_cluster/single-region-aws-cmk.tf @@ -1,5 +1,6 @@ # EAR enabled single region cluster # The same cmk_spec can be used for multi region/read replica clusters as well +# Encryption at rest is supported on clusters with database version 2.16.7.0 or later variable "ysql_password" { type = string diff --git a/examples/resources/ybm_cluster/single-region-azure-cmk.tf b/examples/resources/ybm_cluster/single-region-azure-cmk.tf new file mode 100644 index 0000000..3f87088 --- /dev/null +++ b/examples/resources/ybm_cluster/single-region-azure-cmk.tf @@ -0,0 +1,54 @@ +# EAR enabled single region cluster +# The same cmk_spec can be used for multi region/read replica clusters as well +# Encryption at rest is supported on clusters with database version 2.16.7.0 or later + +variable "ysql_password" { + type = string + description = "YSQL Password." + sensitive = true +} + +variable "ycql_password" { + type = string + description = "YCQL Password." + sensitive = true +} + +resource "ybm_cluster" "single_region" { + cluster_name = "test-cluster-with-azure-cmk" + # The cloud provider for the cluster is indepedent of the CMK Provider + # eg. GCP cluster with AZURE CMK is supported + cloud_type = "GCP" + cluster_type = "SYNCHRONOUS" + cluster_region_info = [ + { + region = "us-west1" + num_nodes = 6 + } + ] + cluster_tier = "PAID" + # fault tolerance cannot be NONE for CMK enabled cluster + fault_tolerance = "ZONE" + cmk_spec = { + provider_type = "AZURE" + azure_cmk_spec = { + client_id = "your-client-id" + client_secret = "your-client-secret" + tenant_id = "your-tenant-id" + key_name = "your-key-name" + key_vault_uri = "your-key-vault-uri" + } + is_enabled = true + } + + node_config = { + num_cores = 4 + disk_size_gb = 50 + } + credentials = { + ysql_username = "example_ysql_user" + ysql_password = var.ysql_password + ycql_username = "example_ycql_user" + ycql_password = var.ycql_password + } +} \ No newline at end of file diff --git a/examples/resources/ybm_cluster/single-region-gcp-cmk.tf b/examples/resources/ybm_cluster/single-region-gcp-cmk.tf index 0b8b591..9e285f2 100644 --- a/examples/resources/ybm_cluster/single-region-gcp-cmk.tf +++ b/examples/resources/ybm_cluster/single-region-gcp-cmk.tf @@ -1,5 +1,6 @@ # EAR enabled single region cluster # The same cmk_spec can be used for multi region/read replica clusters as well +# Encryption at rest is supported on clusters with database version 2.16.7.0 or later variable "ysql_password" { type = string diff --git a/go.mod b/go.mod index 4e10dde..a6cf709 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/terraform-plugin-framework-validators v0.4.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/sethvargo/go-retry v0.2.3 - github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20230915211221-361825bc5618 + github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20231030163130-9268a00de50c ) require ( diff --git a/go.sum b/go.sum index d9fe4d8..bda9639 100644 --- a/go.sum +++ b/go.sum @@ -161,6 +161,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20230915211221-361825bc5618 h1:ubU9s9gJPqDVcl9ZFd6sXZWhvRWJtcgdG5WtVVn4WE4= github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20230915211221-361825bc5618/go.mod h1:5vW0xIzIZw+1djkiWKx0qqNmqbRBSf4mjc4qw8lIMik= +github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20231030163130-9268a00de50c h1:MzOpYz9LCniKMUhWZOnp6kn8bda5zq8vQ0oI58akrtc= +github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20231030163130-9268a00de50c/go.mod h1:5vW0xIzIZw+1djkiWKx0qqNmqbRBSf4mjc4qw8lIMik= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/managed/data_source_cluster_name.go b/managed/data_source_cluster_name.go index 531cd0a..1a30142 100644 --- a/managed/data_source_cluster_name.go +++ b/managed/data_source_cluster_name.go @@ -242,6 +242,37 @@ func (r dataClusterNameType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Di }, }), }, + "azure_cmk_spec": { + Description: "AZURE CMK Provider Configuration.", + Computed: true, + Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{ + "client_id": { + Description: "Client ID", + Type: types.StringType, + Computed: true, + }, + "client_secret": { + Description: "Client Secret", + Type: types.StringType, + Computed: true, + }, + "tenant_id": { + Description: "Tenant ID", + Type: types.StringType, + Computed: true, + }, + "key_vault_uri": { + Description: "Key Vault URI", + Type: types.StringType, + Computed: true, + }, + "key_name": { + Description: "Key Name", + Type: types.StringType, + Computed: true, + }, + }), + }, }), }, "cluster_tier": { diff --git a/managed/models.go b/managed/models.go index 3d542a2..bdae37e 100644 --- a/managed/models.go +++ b/managed/models.go @@ -41,10 +41,11 @@ type ClusterEndpoint struct { } type CMKSpec struct { - ProviderType types.String `tfsdk:"provider_type"` - AWSCMKSpec *AWSCMKSpec `tfsdk:"aws_cmk_spec"` - GCPCMKSpec *GCPCMKSpec `tfsdk:"gcp_cmk_spec"` - IsEnabled types.Bool `tfsdk:"is_enabled"` + ProviderType types.String `tfsdk:"provider_type"` + AWSCMKSpec *AWSCMKSpec `tfsdk:"aws_cmk_spec"` + GCPCMKSpec *GCPCMKSpec `tfsdk:"gcp_cmk_spec"` + AzureCMKSpec *AzureCMKSpec `tfsdk:"azure_cmk_spec"` + IsEnabled types.Bool `tfsdk:"is_enabled"` } type AWSCMKSpec struct { @@ -60,6 +61,15 @@ type GCPCMKSpec struct { ProtectionLevel types.String `tfsdk:"protection_level"` GcpServiceAccount GCPServiceAccount `tfsdk:"gcp_service_account"` } + +type AzureCMKSpec struct { + ClientID types.String `tfsdk:"client_id"` + ClientSecret types.String `tfsdk:"client_secret"` + TenantID types.String `tfsdk:"tenant_id"` + KeyVaultUri types.String `tfsdk:"key_vault_uri"` + KeyName types.String `tfsdk:"key_name"` +} + type GCPServiceAccount struct { Type types.String `tfsdk:"type"` ProjectId types.String `tfsdk:"project_id"` diff --git a/managed/resource_backup.go b/managed/resource_backup.go index bc4f359..16ebde2 100644 --- a/managed/resource_backup.go +++ b/managed/resource_backup.go @@ -144,7 +144,7 @@ func (r resourceBackup) Create(ctx context.Context, req tfsdk.CreateResourceRequ backupRetentionPeriodInDays := int32(plan.RetentionPeriodInDays.Value) backupSpec := *openapiclient.NewBackupSpec(clusterId) - backupSpec.Description = &backupDescription + backupSpec.SetDescription(backupDescription) backupSpec.RetentionPeriodInDays = &backupRetentionPeriodInDays backupResp, response, err := apiClient.BackupApi.CreateBackup(context.Background(), accountId, projectId).BackupSpec(backupSpec).Execute() @@ -203,7 +203,7 @@ func resourceBackupRead(accountId string, projectId string, backupId string, api backup.BackupID.Value = backupId backup.ClusterID.Value = backupResp.Data.Spec.ClusterId - backup.BackupDescription.Value = *(backupResp.Data.Spec.Description) + backup.BackupDescription.Value = *(backupResp.Data.Spec.Description.Get()) backup.RetentionPeriodInDays.Value = int64(*backupResp.Data.Spec.RetentionPeriodInDays) backup.MostRecent.Null = true backup.Timestamp.Null = true diff --git a/managed/resource_cluster.go b/managed/resource_cluster.go index 692f123..46ab553 100644 --- a/managed/resource_cluster.go +++ b/managed/resource_cluster.go @@ -155,7 +155,7 @@ and modify the backup schedule of the cluster being created.`, Description: "CMK Provider Type.", Type: types.StringType, Required: true, - Validators: []tfsdk.AttributeValidator{stringvalidator.OneOf("AWS", "GCP")}, + Validators: []tfsdk.AttributeValidator{stringvalidator.OneOf("AWS", "GCP", "AZURE")}, }, "is_enabled": { Description: "Is Enabled", @@ -270,6 +270,37 @@ and modify the backup schedule of the cluster being created.`, }, }), }, + "azure_cmk_spec": { + Description: "AZURE CMK Provider Configuration.", + Optional: true, + Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{ + "client_id": { + Description: "Azure Active Directory (AD) Client ID for Key Vault service principal.", + Type: types.StringType, + Required: true, + }, + "client_secret": { + Description: "Azure AD Client Secret for Key Vault service principal.", + Type: types.StringType, + Required: true, + }, + "tenant_id": { + Description: "Azure AD Tenant ID for Key Vault service principal.", + Type: types.StringType, + Required: true, + }, + "key_vault_uri": { + Description: "URI of Azure Key Vault storing cryptographic keys.", + Type: types.StringType, + Required: true, + }, + "key_name": { + Description: "Name of cryptographic key in Azure Key Vault.", + Type: types.StringType, + Required: true, + }, + }), + }, }), }, "cluster_tier": { @@ -466,7 +497,7 @@ func EditBackupSchedule(ctx context.Context, backupScheduleStruct BackupSchedule backupRetentionPeriodInDays := int32(backupScheduleStruct.RetentionPeriodInDays.Value) backupDescription := backupDes backupSpec := *openapiclient.NewBackupSpec(clusterId) - backupSpec.Description = &backupDescription + backupSpec.SetDescription(backupDescription) backupSpec.RetentionPeriodInDays = &backupRetentionPeriodInDays scheduleSpec := *openapiclient.NewScheduleSpec(openapiclient.ScheduleStateEnum(backupScheduleStruct.State.Value)) if backupScheduleStruct.TimeIntervalInDays.Value != 0 { @@ -693,12 +724,32 @@ func validateCredentials(credentials Credentials) bool { } +func validateOnlyOneCMKSpec(plan *Cluster) error { + count := 0 + + if plan.CMKSpec.GCPCMKSpec != nil { + count++ + } + if plan.CMKSpec.AWSCMKSpec != nil { + count++ + } + if plan.CMKSpec.AzureCMKSpec != nil { + count++ + } + + if count != 1 { + return errors.New("Invalid input. Only one CMK Provider out of AWS, GCP, or AZURE must be present.") + } + + return nil +} + func createCmkSpec(plan Cluster) (*openapiclient.CMKSpec, error) { cmkProvider := plan.CMKSpec.ProviderType.Value cmkSpec := openapiclient.NewCMKSpec(openapiclient.CMKProviderEnum(cmkProvider)) - if plan.CMKSpec.GCPCMKSpec != nil && plan.CMKSpec.AWSCMKSpec != nil { - return nil, errors.New("Invalid input. Both AWS and GCP spec cannot be present") + if err := validateOnlyOneCMKSpec(&plan); err != nil { + return nil, err } switch cmkProvider { @@ -748,6 +799,18 @@ func createCmkSpec(plan Cluster) (*openapiclient.CMKSpec, error) { awsCmkSpec := openapiclient.NewAWSCMKSpec(awsAccessKey, awsSecretKey, awsArnList) cmkSpec.SetAwsCmkSpec(*awsCmkSpec) + case "AZURE": + if plan.CMKSpec.AzureCMKSpec == nil { + return nil, errors.New("Provider type is AZURE but AZURE CMK spec is missing.") + } + azureClientId := plan.CMKSpec.AzureCMKSpec.ClientID.Value + azureClientSecret := plan.CMKSpec.AzureCMKSpec.ClientSecret.Value + azureTenantId := plan.CMKSpec.AzureCMKSpec.TenantID.Value + azureKeyVaultUri := plan.CMKSpec.AzureCMKSpec.KeyVaultUri.Value + azureKeyName := plan.CMKSpec.AzureCMKSpec.KeyName.Value + + azureCmkSpec := openapiclient.NewAzureCMKSpec(azureClientId, azureClientSecret, azureTenantId, azureKeyVaultUri, azureKeyName) + cmkSpec.SetAzureCmkSpec(*azureCmkSpec) } cmkSpec.SetIsEnabled(plan.CMKSpec.IsEnabled.Value) @@ -1039,6 +1102,10 @@ func (r resourceCluster) Create(ctx context.Context, req tfsdk.CreateResourceReq case "GCP": cluster.CMKSpec.GCPCMKSpec.GcpServiceAccount.ClientId = types.String{Value: string(cmkSpec.GetGcpCmkSpec().GcpServiceAccount.ClientId)} cluster.CMKSpec.GCPCMKSpec.GcpServiceAccount.PrivateKey = types.String{Value: string(*cmkSpec.GetGcpCmkSpec().GcpServiceAccount.PrivateKey)} + case "AZURE": + cluster.CMKSpec.AzureCMKSpec.ClientID = types.String{Value: string(cmkSpec.GetAzureCmkSpec().ClientId)} + cluster.CMKSpec.AzureCMKSpec.ClientSecret = types.String{Value: string(cmkSpec.GetAzureCmkSpec().ClientSecret)} + cluster.CMKSpec.AzureCMKSpec.TenantID = types.String{Value: string(cmkSpec.GetAzureCmkSpec().TenantId)} } } @@ -1245,6 +1312,10 @@ func (r resourceCluster) Read(ctx context.Context, req tfsdk.ReadResourceRequest case "GCP": cluster.CMKSpec.GCPCMKSpec.GcpServiceAccount.ClientId.Value = cmkSpec.GCPCMKSpec.GcpServiceAccount.ClientId.Value cluster.CMKSpec.GCPCMKSpec.GcpServiceAccount.PrivateKey.Value = cmkSpec.GCPCMKSpec.GcpServiceAccount.PrivateKey.Value + case "AZURE": + cluster.CMKSpec.AzureCMKSpec.ClientID = types.String{Value: string(cmkSpec.AzureCMKSpec.ClientID.Value)} + cluster.CMKSpec.AzureCMKSpec.ClientSecret = types.String{Value: string(cmkSpec.AzureCMKSpec.ClientSecret.Value)} + cluster.CMKSpec.AzureCMKSpec.TenantID = types.String{Value: string(cmkSpec.AzureCMKSpec.TenantID.Value)} } } @@ -1374,6 +1445,17 @@ func resourceClusterRead(ctx context.Context, accountId string, projectId string cmkSpec.GCPCMKSpec = &gcpCMKSpec cluster.CMKSpec = &cmkSpec + case "AZURE": + azureCMKSpec := AzureCMKSpec{ + ClientID: types.String{Value: cmkDataSpec.GetAzureCmkSpec().ClientId}, + ClientSecret: types.String{Value: cmkDataSpec.GetAzureCmkSpec().ClientSecret}, + TenantID: types.String{Value: cmkDataSpec.GetAzureCmkSpec().TenantId}, + KeyVaultUri: types.String{Value: cmkDataSpec.GetAzureCmkSpec().KeyVaultUri}, + KeyName: types.String{Value: cmkDataSpec.GetAzureCmkSpec().KeyName}, + } + + cmkSpec.AzureCMKSpec = &azureCMKSpec + cluster.CMKSpec = &cmkSpec } } @@ -1856,7 +1938,8 @@ func (r resourceCluster) Update(ctx context.Context, req tfsdk.UpdateResourceReq tflog.Debug(ctx, "Cluster Update: Allow list IDs read from API server ", map[string]interface{}{ "Allow List IDs": cluster.ClusterAllowListIDs}) - // Update the State file with the unmasked creds for AWS (secret key,access) and GCP (client id,private key) + // Update the State file with the unmasked creds for AWS (Secret Key, Access Key), GCP (Client ID, Private Key) + // and Azure (client ID, client Secret, tenant ID) if plan.CMKSpec != nil { providerType := cluster.CMKSpec.ProviderType.Value switch providerType { @@ -1866,6 +1949,10 @@ func (r resourceCluster) Update(ctx context.Context, req tfsdk.UpdateResourceReq case "GCP": cluster.CMKSpec.GCPCMKSpec.GcpServiceAccount.ClientId = plan.CMKSpec.GCPCMKSpec.GcpServiceAccount.ClientId cluster.CMKSpec.GCPCMKSpec.GcpServiceAccount.PrivateKey = plan.CMKSpec.GCPCMKSpec.GcpServiceAccount.PrivateKey + case "AZURE": + cluster.CMKSpec.AzureCMKSpec.ClientID = plan.CMKSpec.AzureCMKSpec.ClientID + cluster.CMKSpec.AzureCMKSpec.ClientSecret = plan.CMKSpec.AzureCMKSpec.ClientSecret + cluster.CMKSpec.AzureCMKSpec.TenantID = plan.CMKSpec.AzureCMKSpec.TenantID } } diff --git a/mock_yugabytedb_managed_go_client_internal/mock_api_account.go b/mock_yugabytedb_managed_go_client_internal/mock_api_account.go index 23a0fa3..c713b6c 100644 --- a/mock_yugabytedb_managed_go_client_internal/mock_api_account.go +++ b/mock_yugabytedb_managed_go_client_internal/mock_api_account.go @@ -186,6 +186,36 @@ func (mr *MockAccountApiMockRecorder) GetAllowedLoginTypesExecute(arg0 interface return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllowedLoginTypesExecute", reflect.TypeOf((*MockAccountApi)(nil).GetAllowedLoginTypesExecute), arg0) } +// GetLoginRecords mocks base method. +func (m *MockAccountApi) GetLoginRecords(arg0 context.Context, arg1 string) openapi.ApiGetLoginRecordsRequest { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLoginRecords", arg0, arg1) + ret0, _ := ret[0].(openapi.ApiGetLoginRecordsRequest) + return ret0 +} + +// GetLoginRecords indicates an expected call of GetLoginRecords. +func (mr *MockAccountApiMockRecorder) GetLoginRecords(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLoginRecords", reflect.TypeOf((*MockAccountApi)(nil).GetLoginRecords), arg0, arg1) +} + +// GetLoginRecordsExecute mocks base method. +func (m *MockAccountApi) GetLoginRecordsExecute(arg0 openapi.ApiGetLoginRecordsRequest) (openapi.LoginRecordListResponse, *http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLoginRecordsExecute", arg0) + ret0, _ := ret[0].(openapi.LoginRecordListResponse) + ret1, _ := ret[1].(*http.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetLoginRecordsExecute indicates an expected call of GetLoginRecordsExecute. +func (mr *MockAccountApiMockRecorder) GetLoginRecordsExecute(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLoginRecordsExecute", reflect.TypeOf((*MockAccountApi)(nil).GetLoginRecordsExecute), arg0) +} + // InviteAccountUser mocks base method. func (m *MockAccountApi) InviteAccountUser(arg0 context.Context, arg1 string) openapi.ApiInviteAccountUserRequest { m.ctrl.T.Helper()