diff --git a/.github/labeler-issue-triage.yml b/.github/labeler-issue-triage.yml index 730691a195a0..390aa986d72b 100644 --- a/.github/labeler-issue-triage.yml +++ b/.github/labeler-issue-triage.yml @@ -64,7 +64,7 @@ service/cdn: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_cdn_((.|\n)*)###' service/cognitive-services: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_cognitive_((.|\n)*)###' + - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(ai_services|cognitive_)((.|\n)*)###' service/communication: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(communication_service|email_communication_service)((.|\n)*)###' diff --git a/internal/services/cognitive/ai_services_resource.go b/internal/services/cognitive/ai_services_resource.go new file mode 100644 index 000000000000..c3443483bb3b --- /dev/null +++ b/internal/services/cognitive/ai_services_resource.go @@ -0,0 +1,767 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cognitive + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" + "github.com/hashicorp/go-azure-helpers/resourcemanager/location" + "github.com/hashicorp/go-azure-sdk/resource-manager/cognitive/2023-05-01/cognitiveservicesaccounts" + "github.com/hashicorp/go-azure-sdk/sdk/environments" + "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" + commonValidate "github.com/hashicorp/terraform-provider-azurerm/helpers/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/locks" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + cognitiveValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/cognitive/validate" + keyVaultParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/parse" + keyVaultValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/validate" + managedHsmHelpers "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/helpers" + managedHsmParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" + managedHsmValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/network" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/set" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +var _ sdk.ResourceWithUpdate = AzureAIServicesResource{} + +var _ sdk.ResourceWithCustomImporter = AzureAIServicesResource{} + +type AzureAIServicesResource struct{} + +func (r AzureAIServicesResource) CustomImporter() sdk.ResourceRunFunc { + return func(ctx context.Context, metadata sdk.ResourceMetaData) error { + _, err := cognitiveservicesaccounts.ParseAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + return nil + } +} + +type AzureAIServicesVirtualNetworkRules struct { + SubnetID string `tfschema:"subnet_id"` + IgnoreMissingVnetServiceEndpoint bool `tfschema:"ignore_missing_vnet_service_endpoint"` +} + +type AzureAIServicesNetworkACLs struct { + DefaultAction string `tfschema:"default_action"` + IpRules []string `tfschema:"ip_rules"` + VirtualNetworkRules []AzureAIServicesVirtualNetworkRules `tfschema:"virtual_network_rules"` +} + +type AzureAIServicesCustomerManagedKey struct { + IdentityClientID string `tfschema:"identity_client_id"` + KeyVaultKeyID string `tfschema:"key_vault_key_id"` + ManagedHsmKeyID string `tfschema:"managed_hsm_key_id"` +} + +type AzureAIServicesResourceResourceModel struct { + Name string `tfschema:"name"` + ResourceGroupName string `tfschema:"resource_group_name"` + Location string `tfschema:"location"` + SkuName string `tfschema:"sku_name"` + CustomSubdomainName string `tfschema:"custom_subdomain_name"` + CustomerManagedKey []AzureAIServicesCustomerManagedKey `tfschema:"customer_managed_key"` + Fqdns []string `tfschema:"fqdns"` + Identity []identity.ModelSystemAssignedUserAssigned `tfschema:"identity"` + LocalAuthorizationEnabled bool `tfschema:"local_authentication_enabled"` + NetworkACLs []AzureAIServicesNetworkACLs `tfschema:"network_acls"` + OutboundNetworkAccessRestricted bool `tfschema:"outbound_network_access_restricted"` + PublicNetworkAccess string `tfschema:"public_network_access"` + Tags map[string]string `tfschema:"tags"` + Endpoint string `tfschema:"endpoint"` + PrimaryAccessKey string `tfschema:"primary_access_key"` + SecondaryAccessKey string `tfschema:"secondary_access_key"` +} + +func (AzureAIServicesResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: cognitiveValidate.AccountName(), + }, + + "location": commonschema.Location(), + + "resource_group_name": commonschema.ResourceGroupName(), + + "sku_name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "F0", "F1", "S0", "S", "S1", "S2", "S3", "S4", "S5", "S6", "P0", "P1", "P2", "E0", "DC0", + }, false), + }, + + "custom_subdomain_name": { + Type: pluginsdk.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "customer_managed_key": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "key_vault_key_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: keyVaultValidate.NestedItemIdWithOptionalVersion, + ExactlyOneOf: []string{"customer_managed_key.0.managed_hsm_key_id", "customer_managed_key.0.key_vault_key_id"}, + }, + + "managed_hsm_key_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.Any(managedHsmValidate.ManagedHSMDataPlaneVersionedKeyID, managedHsmValidate.ManagedHSMDataPlaneVersionlessKeyID), + ExactlyOneOf: []string{"customer_managed_key.0.managed_hsm_key_id", "customer_managed_key.0.key_vault_key_id"}, + }, + + "identity_client_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + }, + }, + }, + }, + + "fqdns": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + + "identity": commonschema.SystemAssignedUserAssignedIdentityOptional(), + + "local_authentication_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "network_acls": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + RequiredWith: []string{"custom_subdomain_name"}, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "default_action": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(cognitiveservicesaccounts.NetworkRuleActionAllow), + string(cognitiveservicesaccounts.NetworkRuleActionDeny), + }, false), + }, + "ip_rules": { + Type: pluginsdk.TypeSet, + Optional: true, + Set: set.HashIPv4AddressOrCIDR, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.Any( + commonValidate.IPv4Address, + commonValidate.CIDR, + ), + }, + }, + + "virtual_network_rules": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "subnet_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "ignore_missing_vnet_service_endpoint": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + }, + }, + }, + }, + }, + }, + + "outbound_network_access_restricted": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + + "public_network_access": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(cognitiveservicesaccounts.PublicNetworkAccessEnabled), + string(cognitiveservicesaccounts.PublicNetworkAccessDisabled), + }, false), + Default: string(cognitiveservicesaccounts.PublicNetworkAccessEnabled), + }, + + "storage": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "storage_account_id": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: commonids.ValidateStorageAccountID, + }, + + "identity_client_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + }, + }, + }, + }, + + "tags": commonschema.Tags(), + } +} + +func (AzureAIServicesResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "endpoint": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "primary_access_key": { + Type: pluginsdk.TypeString, + Computed: true, + Sensitive: true, + }, + + "secondary_access_key": { + Type: pluginsdk.TypeString, + Computed: true, + Sensitive: true, + }, + } +} + +func (AzureAIServicesResource) ModelObject() interface{} { + return &AzureAIServicesResourceResourceModel{} +} + +func (AzureAIServicesResource) ResourceType() string { + return "azurerm_ai_services" +} + +func (AzureAIServicesResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 180 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var model AzureAIServicesResourceResourceModel + if err := metadata.Decode(&model); err != nil { + return err + } + + client := metadata.Client.Cognitive.AccountsClient + subscriptionId := metadata.Client.Account.SubscriptionId + + id := cognitiveservicesaccounts.NewAccountID(subscriptionId, model.ResourceGroupName, model.Name) + existing, err := client.AccountsGet(ctx, id) + if err != nil { + if !response.WasNotFound(existing.HttpResponse) { + return fmt.Errorf("checking for presence of existing %s: %+v", id, err) + } + } + + if !response.WasNotFound(existing.HttpResponse) { + return tf.ImportAsExistsError("azurerm_ai_services", id.ID()) + } + + networkACLs, subnetIds := expandAzureAIServicesNetworkACLs(model.NetworkACLs) + + // also lock on the Virtual Network ID's since modifications in the networking stack are exclusive + virtualNetworkNames := make([]string, 0) + for _, v := range subnetIds { + subnetId, err := commonids.ParseSubnetID(v) + if err != nil { + return err + } + if !utils.SliceContainsValue(virtualNetworkNames, subnetId.VirtualNetworkName) { + virtualNetworkNames = append(virtualNetworkNames, subnetId.VirtualNetworkName) + } + } + + locks.MultipleByName(&virtualNetworkNames, network.VirtualNetworkResourceName) + defer locks.UnlockMultipleByName(&virtualNetworkNames, network.VirtualNetworkResourceName) + + props := cognitiveservicesaccounts.Account{ + Kind: pointer.To("AIServices"), + Location: pointer.To(location.Normalize(model.Location)), + Sku: &cognitiveservicesaccounts.Sku{ + Name: model.SkuName, + }, + Properties: &cognitiveservicesaccounts.AccountProperties{ + NetworkAcls: networkACLs, + CustomSubDomainName: pointer.To(model.CustomSubdomainName), + AllowedFqdnList: pointer.To(model.Fqdns), + PublicNetworkAccess: pointer.To(cognitiveservicesaccounts.PublicNetworkAccess(model.PublicNetworkAccess)), + RestrictOutboundNetworkAccess: pointer.To(model.OutboundNetworkAccessRestricted), + DisableLocalAuth: pointer.To(!model.LocalAuthorizationEnabled), + }, + Tags: pointer.To(model.Tags), + } + + expandIdentity, err := identity.ExpandSystemAndUserAssignedMapFromModel(model.Identity) + if err != nil { + return fmt.Errorf("expanding `identity`: %+v", err) + } + props.Identity = expandIdentity + + if err := client.AccountsCreateThenPoll(ctx, id, props); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + // creating with KV HSM takes more time than expected, at least hours in most cases and eventually terminated by service + customerManagedKey, err := expandAzureAIServicesCustomerManagedKey(model.CustomerManagedKey) + if err != nil { + return fmt.Errorf("expanding `customer_managed_key`: %+v", err) + } + + if customerManagedKey != nil { + props.Properties.Encryption = customerManagedKey + if err := client.AccountsUpdateThenPoll(ctx, id, props); err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } + } + + metadata.SetID(id) + + return nil + }, + } +} + +func (AzureAIServicesResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Cognitive.AccountsClient + env := metadata.Client.Account.Environment + + state := AzureAIServicesResourceResourceModel{} + id, err := cognitiveservicesaccounts.ParseAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + resp, err := client.AccountsGet(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + keys, err := client.AccountsListKeys(ctx, *id) + if err != nil { + return fmt.Errorf("listing the Keys for %s: %+v", id, err) + } + + if model := keys.Model; model != nil { + state.PrimaryAccessKey = pointer.From(model.Key1) + state.SecondaryAccessKey = pointer.From(model.Key2) + } + + state.Name = id.AccountName + state.ResourceGroupName = id.ResourceGroupName + + if model := resp.Model; model != nil { + state.Location = location.NormalizeNilable(model.Location) + if sku := model.Sku; sku != nil { + state.SkuName = sku.Name + } + + identityFlatten, err := identity.FlattenSystemAndUserAssignedMapToModel(model.Identity) + if err != nil { + return fmt.Errorf("flattening `identity`: %+v", err) + } + state.Identity = *identityFlatten + + if props := model.Properties; props != nil { + state.Endpoint = pointer.From(props.Endpoint) + state.CustomSubdomainName = pointer.From(props.CustomSubDomainName) + state.NetworkACLs = flattenAzureAIServicesNetworkACLs(props.NetworkAcls) + state.Fqdns = pointer.From(props.AllowedFqdnList) + + state.PublicNetworkAccess = string(pointer.From(props.PublicNetworkAccess)) + state.OutboundNetworkAccessRestricted = pointer.From(props.RestrictOutboundNetworkAccess) + + localAuthEnabled := true + if props.DisableLocalAuth != nil { + localAuthEnabled = !*props.DisableLocalAuth + } + state.LocalAuthorizationEnabled = localAuthEnabled + + customerManagedKey, err := flattenAzureAIServicesCustomerManagedKey(props.Encryption, env) + if err != nil { + return fmt.Errorf("flattening `customer_managed_key`: %+v", err) + } + state.CustomerManagedKey = customerManagedKey + } + + state.Tags = pointer.From(model.Tags) + } + + return metadata.Encode(&state) + }, + } +} + +func (AzureAIServicesResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 180 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Cognitive.AccountsClient + + var model AzureAIServicesResourceResourceModel + + if err := metadata.Decode(&model); err != nil { + return err + } + + id, err := cognitiveservicesaccounts.ParseAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + resp, err := client.AccountsGet(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + props := resp.Model + if metadata.ResourceData.HasChange("network_acls") { + networkACLs, subnetIds := expandAzureAIServicesNetworkACLs(model.NetworkACLs) + locks.MultipleByName(&subnetIds, network.VirtualNetworkResourceName) + defer locks.UnlockMultipleByName(&subnetIds, network.VirtualNetworkResourceName) + + // also lock on the Virtual Network ID's since modifications in the networking stack are exclusive + virtualNetworkNames := make([]string, 0) + for _, v := range subnetIds { + subnetId, err := commonids.ParseSubnetIDInsensitively(v) + if err != nil { + return err + } + if !utils.SliceContainsValue(virtualNetworkNames, subnetId.VirtualNetworkName) { + virtualNetworkNames = append(virtualNetworkNames, subnetId.VirtualNetworkName) + } + } + + locks.MultipleByName(&virtualNetworkNames, network.VirtualNetworkResourceName) + defer locks.UnlockMultipleByName(&virtualNetworkNames, network.VirtualNetworkResourceName) + + props.Properties.NetworkAcls = networkACLs + } + + if metadata.ResourceData.HasChange("sku_name") { + props.Sku = &cognitiveservicesaccounts.Sku{ + Name: model.SkuName, + } + } + + if metadata.ResourceData.HasChange("custom_subdomain_name") { + props.Properties.CustomSubDomainName = pointer.FromString(model.CustomSubdomainName) + } + + if metadata.ResourceData.HasChange("fqdns") { + props.Properties.AllowedFqdnList = pointer.To(model.Fqdns) + } + + if metadata.ResourceData.HasChange("public_network_access") { + props.Properties.PublicNetworkAccess = pointer.To(cognitiveservicesaccounts.PublicNetworkAccess(model.PublicNetworkAccess)) + } + + if metadata.ResourceData.HasChange("outbound_network_access_restricted") { + props.Properties.RestrictOutboundNetworkAccess = pointer.To(model.OutboundNetworkAccessRestricted) + } + + if metadata.ResourceData.HasChange("local_authentication_enabled") { + props.Properties.DisableLocalAuth = pointer.To(!model.LocalAuthorizationEnabled) + } + + if metadata.ResourceData.HasChange("tags") { + props.Tags = pointer.To(model.Tags) + } + + if metadata.ResourceData.HasChange("customer_managed_key") { + customerManagedKey, err := expandAzureAIServicesCustomerManagedKey(model.CustomerManagedKey) + if err != nil { + return fmt.Errorf("expanding `customer_managed_key`: %+v", err) + } + + if customerManagedKey != nil { + props.Properties.Encryption = customerManagedKey + } + } + + if metadata.ResourceData.HasChange("identity") { + expandIdentity, err := identity.ExpandSystemAndUserAssignedMapFromModel(model.Identity) + if err != nil { + return fmt.Errorf("expanding `identity`: %+v", err) + } + props.Identity = expandIdentity + } + + if err := client.AccountsUpdateThenPoll(ctx, *id, *props); err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } + + return nil + }, + } +} + +func (AzureAIServicesResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 180 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Cognitive.AccountsClient + + id, err := cognitiveservicesaccounts.ParseAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + log.Printf("[DEBUG] Retrieving %s..", *id) + account, err := client.AccountsGet(ctx, *id) + if err != nil || account.Model == nil || account.Model.Location == nil { + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + deletedAzureAIServicesId := cognitiveservicesaccounts.NewDeletedAccountID(id.SubscriptionId, *account.Model.Location, id.ResourceGroupName, id.AccountName) + if err != nil { + return err + } + + log.Printf("[DEBUG] Deleting %s..", *id) + if err := client.AccountsDeleteThenPoll(ctx, *id); err != nil { + return fmt.Errorf("deleting %s: %+v", *id, err) + } + if metadata.Client.Features.CognitiveAccount.PurgeSoftDeleteOnDestroy { + log.Printf("[DEBUG] Purging %s..", *id) + if err := client.DeletedAccountsPurgeThenPoll(ctx, deletedAzureAIServicesId); err != nil { + return fmt.Errorf("purging %s: %+v", *id, err) + } + } else { + log.Printf("[DEBUG] Skipping Purge of %s", *id) + } + + return nil + }, + } +} + +func (AzureAIServicesResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return cognitiveservicesaccounts.ValidateAccountID +} + +func expandAzureAIServicesCustomerManagedKey(input []AzureAIServicesCustomerManagedKey) (*cognitiveservicesaccounts.Encryption, error) { + if len(input) == 0 { + return &cognitiveservicesaccounts.Encryption{ + KeySource: pointer.To(cognitiveservicesaccounts.KeySourceMicrosoftPointCognitiveServices), + }, nil + } + + v := input[0] + + var identityClientId string + if value := v.IdentityClientID; value != "" { + identityClientId = value + } + + encryption := &cognitiveservicesaccounts.Encryption{ + KeySource: pointer.To(cognitiveservicesaccounts.KeySourceMicrosoftPointKeyVault), + KeyVaultProperties: &cognitiveservicesaccounts.KeyVaultProperties{ + IdentityClientId: pointer.To(identityClientId), + }, + } + + if v.KeyVaultKeyID != "" { + keyId, err := keyVaultParse.ParseOptionallyVersionedNestedItemID(v.KeyVaultKeyID) + if err != nil { + return nil, err + } + encryption.KeyVaultProperties.KeyName = pointer.To(keyId.Name) + encryption.KeyVaultProperties.KeyVersion = pointer.To(keyId.Version) + encryption.KeyVaultProperties.KeyVaultUri = pointer.To(keyId.KeyVaultBaseUrl) + } else { + hsmKyId, err := managedHsmParse.ManagedHSMDataPlaneVersionedKeyID(v.ManagedHsmKeyID, nil) + if err != nil { + return nil, err + } + + encryption.KeyVaultProperties.KeyName = pointer.To(hsmKyId.KeyName) + encryption.KeyVaultProperties.KeyVersion = pointer.To(hsmKyId.KeyVersion) + encryption.KeyVaultProperties.KeyVaultUri = pointer.To(hsmKyId.BaseUri()) + } + return encryption, nil + +} + +func flattenAzureAIServicesCustomerManagedKey(input *cognitiveservicesaccounts.Encryption, env environments.Environment) ([]AzureAIServicesCustomerManagedKey, error) { + if input == nil || *input.KeySource == cognitiveservicesaccounts.KeySourceMicrosoftPointCognitiveServices { + return []AzureAIServicesCustomerManagedKey{}, nil + } + + keyName := "" + keyVaultURI := "" + keyVersion := "" + customerManagerKey := AzureAIServicesCustomerManagedKey{} + + if props := input.KeyVaultProperties; props != nil { + if props.KeyName != nil { + keyName = *props.KeyName + } + if props.KeyVaultUri != nil { + keyVaultURI = *props.KeyVaultUri + } + if props.KeyVersion != nil { + keyVersion = *props.KeyVersion + } + + isHsmURI, err, instanceName, domainSuffix := managedHsmHelpers.IsManagedHSMURI(env, keyVaultURI) + if err != nil { + return nil, err + } + + if props.IdentityClientId != nil { + customerManagerKey.IdentityClientID = *props.IdentityClientId + } + + switch { + case isHsmURI && keyVersion == "": + { + keyVaultKeyId := managedHsmParse.NewManagedHSMDataPlaneVersionlessKeyID(instanceName, domainSuffix, keyName) + customerManagerKey.ManagedHsmKeyID = keyVaultKeyId.ID() + } + case isHsmURI && keyVersion != "": + { + keyVaultKeyId := managedHsmParse.NewManagedHSMDataPlaneVersionedKeyID(instanceName, domainSuffix, keyName, keyVersion) + customerManagerKey.ManagedHsmKeyID = keyVaultKeyId.ID() + } + case !isHsmURI: + { + keyVaultKeyId, err := keyVaultParse.NewNestedItemID(keyVaultURI, keyVaultParse.NestedItemTypeKey, keyName, keyVersion) + if err != nil { + return nil, fmt.Errorf("parsing `key_vault_key_id`: %+v", err) + } + customerManagerKey.KeyVaultKeyID = keyVaultKeyId.ID() + } + } + } + + return []AzureAIServicesCustomerManagedKey{customerManagerKey}, nil +} + +func expandAzureAIServicesNetworkACLs(input []AzureAIServicesNetworkACLs) (*cognitiveservicesaccounts.NetworkRuleSet, []string) { + subnetIds := make([]string, 0) + if len(input) == 0 { + return nil, subnetIds + } + + v := input[0] + + defaultAction := cognitiveservicesaccounts.NetworkRuleAction(v.DefaultAction) + + ipRules := make([]cognitiveservicesaccounts.IPRule, 0) + + for _, val := range v.IpRules { + rule := cognitiveservicesaccounts.IPRule{ + Value: val, + } + ipRules = append(ipRules, rule) + } + + networkRules := make([]cognitiveservicesaccounts.VirtualNetworkRule, 0) + for _, val := range v.VirtualNetworkRules { + subnetId := val.SubnetID + subnetIds = append(subnetIds, subnetId) + rule := cognitiveservicesaccounts.VirtualNetworkRule{ + Id: subnetId, + IgnoreMissingVnetServiceEndpoint: pointer.To(val.IgnoreMissingVnetServiceEndpoint), + } + networkRules = append(networkRules, rule) + } + + ruleSet := cognitiveservicesaccounts.NetworkRuleSet{ + DefaultAction: &defaultAction, + IPRules: &ipRules, + VirtualNetworkRules: &networkRules, + } + return &ruleSet, subnetIds +} + +func flattenAzureAIServicesNetworkACLs(input *cognitiveservicesaccounts.NetworkRuleSet) []AzureAIServicesNetworkACLs { + if input == nil { + return []AzureAIServicesNetworkACLs{} + } + + ipRules := make([]string, 0) + if input.IPRules != nil { + for _, v := range *input.IPRules { + ipRules = append(ipRules, v.Value) + } + } + + virtualNetworkRules := make([]AzureAIServicesVirtualNetworkRules, 0) + if input.VirtualNetworkRules != nil { + for _, v := range *input.VirtualNetworkRules { + id := v.Id + subnetId, err := commonids.ParseSubnetIDInsensitively(v.Id) + if err == nil { + id = subnetId.ID() + } + + virtualNetworkRules = append(virtualNetworkRules, AzureAIServicesVirtualNetworkRules{ + SubnetID: id, + IgnoreMissingVnetServiceEndpoint: pointer.From(v.IgnoreMissingVnetServiceEndpoint), + }) + } + } + + return []AzureAIServicesNetworkACLs{{ + DefaultAction: string(*input.DefaultAction), + IpRules: ipRules, + VirtualNetworkRules: virtualNetworkRules, + }} +} diff --git a/internal/services/cognitive/ai_services_resource_test.go b/internal/services/cognitive/ai_services_resource_test.go new file mode 100644 index 000000000000..0873df63cfe9 --- /dev/null +++ b/internal/services/cognitive/ai_services_resource_test.go @@ -0,0 +1,814 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cognitive_test + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-sdk/resource-manager/cognitive/2023-05-01/cognitiveservicesaccounts" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type AzureAIServicesResource struct{} + +func TestAccCognitiveAzureAIServices_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_ai_services", "test") + r := AzureAIServicesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("tags.%").HasValue("0"), + check.That(data.ResourceName).Key("primary_access_key").Exists(), + check.That(data.ResourceName).Key("secondary_access_key").Exists(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccCognitiveAzureAIServices_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_ai_services", "test") + r := AzureAIServicesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.requiresImport(data), + ExpectError: acceptance.RequiresImportError("azurerm_ai_services"), + }, + }) +} + +func TestAccCognitiveAzureAIServices_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_ai_services", "test") + r := AzureAIServicesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("tags.%").HasValue("1"), + check.That(data.ResourceName).Key("tags.Acceptance").HasValue("Test"), + check.That(data.ResourceName).Key("primary_access_key").Exists(), + check.That(data.ResourceName).Key("secondary_access_key").Exists(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccCognitiveAzureAIServices_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_ai_services", "test") + r := AzureAIServicesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("tags.%").HasValue("0"), + check.That(data.ResourceName).Key("primary_access_key").Exists(), + check.That(data.ResourceName).Key("secondary_access_key").Exists(), + ), + }, + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("tags.%").HasValue("1"), + check.That(data.ResourceName).Key("tags.Acceptance").HasValue("Test"), + check.That(data.ResourceName).Key("primary_access_key").Exists(), + check.That(data.ResourceName).Key("secondary_access_key").Exists(), + ), + }, + }) +} + +func TestAccCognitiveAzureAIServices_networkACLs(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_ai_services", "test") + r := AzureAIServicesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.networkACLs(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.networkACLsUpdated(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccCognitiveAzureAIServices_identity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_ai_services", "test") + r := AzureAIServicesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.identitySystemAssignedUserAssigned(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("identity.0.principal_id").IsUUID(), + check.That(data.ResourceName).Key("identity.0.tenant_id").IsUUID(), + ), + }, + data.ImportStep(), + { + Config: r.identityUserAssigned(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.identitySystemAssigned(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("identity.0.principal_id").IsUUID(), + check.That(data.ResourceName).Key("identity.0.tenant_id").IsUUID(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccCognitiveAzureAIServices_customerManagedKey_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_ai_services", "test") + r := AzureAIServicesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.customerManagedKey(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("customer_managed_key.0.key_vault_key_id").Exists(), + check.That(data.ResourceName).Key("customer_managed_key.0.identity_client_id").IsUUID(), + ), + }, + data.ImportStep(), + { + Config: r.customerManagedKeyUpdate(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.customerManagedKey(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("customer_managed_key.0.key_vault_key_id").Exists(), + check.That(data.ResourceName).Key("customer_managed_key.0.identity_client_id").IsUUID(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccCognitiveAzureAIServices_KVHsmManagedKey(t *testing.T) { + if os.Getenv("ARM_TEST_HSM_KEY") == "" { + t.Skip("Skipping as ARM_TEST_HSM_KEY is not specified") + return + } + data := acceptance.BuildTestData(t, "azurerm_ai_services", "test") + r := AzureAIServicesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.kvHsmManagedKey(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("customer_managed_key.0.managed_hsm_key_id").Exists(), + check.That(data.ResourceName).Key("customer_managed_key.0.identity_client_id").IsUUID(), + ), + }, + data.ImportStep(), + }) +} + +func (AzureAIServicesResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := cognitiveservicesaccounts.ParseAccountID(state.ID) + if err != nil { + return nil, err + } + + resp, err := clients.Cognitive.AccountsClient.AccountsGet(ctx, *id) + if err != nil { + return nil, fmt.Errorf("retrieving %s: %+v", *id, err) + } + + return pointer.To(resp.Model != nil), nil +} + +func (AzureAIServicesResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-cognitive-%d" + location = "%s" +} + +resource "azurerm_ai_services" "test" { + name = "acctestcogacc-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "S0" +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} + +func (AzureAIServicesResource) identitySystemAssigned(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-cognitive-%d" + location = "%s" +} + +resource "azurerm_ai_services" "test" { + name = "acctestcogacc-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "S0" + identity { + type = "SystemAssigned" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} + +func (AzureAIServicesResource) identityUserAssigned(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-cognitive-%d" + location = "%s" +} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctestUAI-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_ai_services" "test" { + name = "acctestcogacc-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "S0" + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +} + +func (AzureAIServicesResource) identitySystemAssignedUserAssigned(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-cognitive-%d" + location = "%s" +} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctestUAI-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_ai_services" "test" { + name = "acctestcogacc-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "S0" + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +} + +func (AzureAIServicesResource) requiresImport(data acceptance.TestData) string { + template := AzureAIServicesResource{}.basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_ai_services" "import" { + name = azurerm_ai_services.test.name + location = azurerm_ai_services.test.location + resource_group_name = azurerm_ai_services.test.resource_group_name + sku_name = "S0" +} +`, template) +} + +func (AzureAIServicesResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-cognitive-%[1]d" + location = "%[2]s" +} + +resource "azurerm_user_assigned_identity" "test" { + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + name = "%[3]s" +} + +resource "azurerm_key_vault" "test" { + name = "acctestkv%[3]s" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + purge_protection_enabled = true + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + key_permissions = [ + "Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy" + ] + secret_permissions = [ + "Get", + ] + } + + access_policy { + tenant_id = azurerm_user_assigned_identity.test.tenant_id + object_id = azurerm_user_assigned_identity.test.principal_id + key_permissions = [ + "Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy" + ] + secret_permissions = [ + "Get", + ] + } +} + +resource "azurerm_key_vault_key" "test" { + name = "acctestkvkey%[3]s" + key_vault_id = azurerm_key_vault.test.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"] +} + +resource "azurerm_virtual_network" "test" { + name = "acctestvirtnet%[1]d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test_a" { + name = "acctestsubneta%[1]d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] + service_endpoints = ["Microsoft.CognitiveServices"] +} + +resource "azurerm_subnet" "test_b" { + name = "acctestsubnetb%[1]d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.4.0/24"] + service_endpoints = ["Microsoft.CognitiveServices"] +} + +resource "azurerm_ai_services" "test" { + name = "acctestcogacc-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "S0" + fqdns = ["foo.com", "bar.com"] + local_authentication_enabled = false + outbound_network_access_restricted = false + public_network_access = "Disabled" + custom_subdomain_name = "acctestcogacc-%[1]d" + + customer_managed_key { + key_vault_key_id = azurerm_key_vault_key.test.id + identity_client_id = azurerm_user_assigned_identity.test.client_id + } + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id + ] + } + network_acls { + default_action = "Deny" + virtual_network_rules { + subnet_id = azurerm_subnet.test_a.id + } + virtual_network_rules { + subnet_id = azurerm_subnet.test_b.id + } + } + + tags = { + Acceptance = "Test" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomIntOfLength(8)) +} + +func (r AzureAIServicesResource) networkACLs(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_ai_services" "test" { + name = "acctestcogacc-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "S0" + custom_subdomain_name = "acctestcogacc-%d" + + network_acls { + default_action = "Deny" + virtual_network_rules { + subnet_id = azurerm_subnet.test_a.id + } + virtual_network_rules { + subnet_id = azurerm_subnet.test_b.id + } + } +} +`, r.networkACLsTemplate(data), data.RandomInteger, data.RandomInteger) +} + +func (r AzureAIServicesResource) networkACLsUpdated(data acceptance.TestData) string { + return fmt.Sprintf(` +%s +resource "azurerm_ai_services" "test" { + name = "acctestcogacc-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "S0" + custom_subdomain_name = "acctestcogacc-%d" + + network_acls { + default_action = "Allow" + ip_rules = ["123.0.0.101"] + virtual_network_rules { + subnet_id = azurerm_subnet.test_a.id + } + virtual_network_rules { + subnet_id = azurerm_subnet.test_b.id + } + } +} +`, r.networkACLsTemplate(data), data.RandomInteger, data.RandomInteger) +} + +func (AzureAIServicesResource) networkACLsTemplate(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" { +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-cognitive-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctestvirtnet%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test_a" { + name = "acctestsubneta%d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] + service_endpoints = ["Microsoft.CognitiveServices"] +} + +resource "azurerm_subnet" "test_b" { + name = "acctestsubnetb%d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.4.0/24"] + service_endpoints = ["Microsoft.CognitiveServices"] +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomInteger) +} + +func (AzureAIServicesResource) customerManagedKey(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + key_vault { + purge_soft_delete_on_destroy = false + purge_soft_deleted_keys_on_destroy = false + } + } +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-cognitive-%d" + location = "%s" +} + +resource "azurerm_user_assigned_identity" "test" { + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + name = "%s" +} + +resource "azurerm_key_vault" "test" { + name = "acctestkv%s" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + purge_protection_enabled = true + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + key_permissions = [ + "Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy" + ] + secret_permissions = [ + "Get", + ] + } + + access_policy { + tenant_id = azurerm_user_assigned_identity.test.tenant_id + object_id = azurerm_user_assigned_identity.test.principal_id + key_permissions = [ + "Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy" + ] + secret_permissions = [ + "Get", + ] + } +} + +resource "azurerm_key_vault_key" "test" { + name = "acctestkvkey%s" + key_vault_id = azurerm_key_vault.test.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"] +} + +resource "azurerm_ai_services" "test" { + name = "acctest-cogacc-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "S0" + custom_subdomain_name = "acctest-cogacc-%d" + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id + ] + } + + customer_managed_key { + key_vault_key_id = azurerm_key_vault_key.test.id + identity_client_id = azurerm_user_assigned_identity.test.client_id + } +} +`, data.RandomInteger, data.Locations.Secondary, data.RandomString, data.RandomString, data.RandomString, data.RandomInteger, data.RandomInteger) +} + +func (AzureAIServicesResource) customerManagedKeyUpdate(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + key_vault { + purge_soft_delete_on_destroy = false + purge_soft_deleted_keys_on_destroy = false + } + } +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-cognitive-%d" + location = "%s" +} + +resource "azurerm_user_assigned_identity" "test" { + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + name = "%s" +} + +resource "azurerm_key_vault" "test" { + name = "acctestkv%s" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + purge_protection_enabled = true + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + key_permissions = [ + "Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy" + ] + secret_permissions = [ + "Get", + ] + } + + access_policy { + tenant_id = azurerm_user_assigned_identity.test.tenant_id + object_id = azurerm_user_assigned_identity.test.principal_id + key_permissions = [ + "Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy" + ] + secret_permissions = [ + "Get", + ] + } +} + +resource "azurerm_key_vault_key" "test" { + name = "acctestkvkey%s" + key_vault_id = azurerm_key_vault.test.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"] +} + +resource "azurerm_ai_services" "test" { + name = "acctest-cogacc-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "S0" + custom_subdomain_name = "acctest-cogacc-%d" + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id + ] + } +} +`, data.RandomInteger, data.Locations.Secondary, data.RandomString, data.RandomString, data.RandomString, data.RandomInteger, data.RandomInteger) +} + +func (AzureAIServicesResource) kvHsmManagedKey(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + key_vault { + purge_soft_delete_on_destroy = false + purge_soft_deleted_keys_on_destroy = false + } + } +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-cognitive-%[1]d" + location = "%[2]s" +} + +resource "azurerm_user_assigned_identity" "test" { + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + name = "%[3]s" +} + +resource "azurerm_key_vault" "test" { + name = "acctestkv%[3]s" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + purge_protection_enabled = true + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + key_permissions = [ + "Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy" + ] + certificate_permissions = [ + "Get", + "Create", + "Delete", + "Recover", + "List" + ] + } + + access_policy { + tenant_id = azurerm_user_assigned_identity.test.tenant_id + object_id = azurerm_user_assigned_identity.test.principal_id + key_permissions = [ + "Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy" + ] + certificate_permissions = [ + "Get", + "Create", + "Delete", + "Recover", + "List" + ] + } +} + +resource "azurerm_ai_services" "test" { + name = "acctest-cogacc-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "S0" + custom_subdomain_name = "acctest-cogacc-%[1]d" + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id + ] + } + + customer_managed_key { + managed_hsm_key_id = "%[4]s" + identity_client_id = azurerm_user_assigned_identity.test.client_id + } +} +`, data.RandomInteger, data.Locations.Secondary, data.RandomString, os.Getenv("ARM_TEST_HSM_KEY")) +} diff --git a/internal/services/cognitive/registration.go b/internal/services/cognitive/registration.go index e696d00869a1..5c3cd5d03c5a 100644 --- a/internal/services/cognitive/registration.go +++ b/internal/services/cognitive/registration.go @@ -54,6 +54,7 @@ func (r Registration) DataSources() []sdk.DataSource { // Resources returns a list of Resources supported by this Service func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ + AzureAIServicesResource{}, CognitiveDeploymentResource{}, } } diff --git a/website/docs/r/ai_services.html.markdown b/website/docs/r/ai_services.html.markdown new file mode 100644 index 000000000000..8110de3b1d94 --- /dev/null +++ b/website/docs/r/ai_services.html.markdown @@ -0,0 +1,150 @@ +--- +subcategory: "Cognitive Services" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_ai_services" +description: |- + Manages an AI Services Account. +--- + +# azurerm_ai_services + +Manages an AI Services account. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_ai_services" "example" { + name = "example-account" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + sku_name = "S0" + + tags = { + Acceptance = "Test" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of the AI Services Account. Changing this forces a new resource to be created. + +* `resource_group_name` - (Required) The name of the resource group in which the AI Services Account is created. Changing this forces a new resource to be created. + +* `location` - (Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created. + +* `sku_name` - (Required) Specifies the SKU Name for this AI Services Account. Possible values are `F0`, `F1`, `S0`, `S`, `S1`, `S2`, `S3`, `S4`, `S5`, `S6`, `P0`, `P1`, `P2`, `E0` and `DC0`. + +-> **NOTE:** SKU `DC0` is the commitment tier for AI Services Account containers running in disconnected environments. You must obtain approval from Microsoft by submitting the [request form](https://aka.ms/csdisconnectedcontainers) first, before you can use this SKU. More information on [Purchase a commitment plan to use containers in disconnected environments](https://learn.microsoft.com/en-us/azure/cognitive-services/containers/disconnected-containers?tabs=stt#purchase-a-commitment-plan-to-use-containers-in-disconnected-environments). + +* `custom_subdomain_name` - (Optional) The subdomain name used for token-based authentication. This property is required when `network_acls` is specified. Changing this forces a new resource to be created. + +* `customer_managed_key` - (Optional) A `customer_managed_key` block as documented below. + +* `fqdns` - (Optional) List of FQDNs allowed for the AI Services Account. + +* `identity` - (Optional) An `identity` block as defined below. + +* `local_authentication_enabled` - (Optional) Whether local authentication is enabled for the AI Services Account. Defaults to `true`. + +* `network_acls` - (Optional) A `network_acls` block as defined below. When this property is specified, `custom_subdomain_name` is also required to be set. + +* `outbound_network_access_restricted` - (Optional) Whether outbound network access is restricted for the AI Services Account. Defaults to `false`. + +* `public_network_access` - (Optional) Whether public network access is allowed for the AI Services Account. Possible values are `Enabled` and `Disabled`. Defaults to `Enabled`. + +* `storage` - (Optional) A `storage` block as defined below. + +* `tags` - (Optional) A mapping of tags to assign to the resource. + +--- + +A `network_acls` block supports the following: + +* `default_action` - (Required) The Default Action to use when no rules match from `ip_rules` / `virtual_network_rules`. Possible values are `Allow` and `Deny`. + +* `ip_rules` - (Optional) One or more IP Addresses, or CIDR Blocks which should be able to access the AI Services Account. + +* `virtual_network_rules` - (Optional) A `virtual_network_rules` block as defined below. + +--- + +A `virtual_network_rules` block supports the following: + +* `subnet_id` - (Required) The ID of the subnet which should be able to access this AI Services Account. + +* `ignore_missing_vnet_service_endpoint` - (Optional) Whether to ignore a missing Virtual Network Service Endpoint or not. Default to `false`. + +--- + +A `customer_managed_key` block supports the following: + +* `key_vault_key_id` - (Optional) The ID of the Key Vault Key which should be used to encrypt the data in this AI Services Account. Exactly one of `key_vault_key_id`, `managed_hsm_key_id` must be specified. + +* `managed_hsm_key_id` - (Optional) The ID of the managed HSM Key which should be used to encrypt the data in this AI Services Account. Exactly one of `key_vault_key_id`, `managed_hsm_key_id` must be specified. + +* `identity_client_id` - (Optional) The Client ID of the User Assigned Identity that has access to the key. This property only needs to be specified when there are multiple identities attached to the Azure AI Service. + +--- + +A `identity` block supports the following: + +* `type` - (Required) Specifies the type of Managed Service Identity that should be configured on this AI Services Account. Possible values are `SystemAssigned`, `UserAssigned`, `SystemAssigned, UserAssigned` + +* `identity_ids` - (Optional) Specifies a list of User Assigned Managed Identity IDs to be assigned to this AI Services Account. + +~> **NOTE:** This is required when `type` is set to `UserAssigned` or `SystemAssigned, UserAssigned`. + +--- + +A `storage` block supports the following: + +* `storage_account_id` - (Required) The ID of the Storage Account. + +* `identity_client_id` - (Optional) The client ID of the Managed Identity associated with the Storage Account. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the AI Services Account. + +* `endpoint` - The endpoint used to connect to the AI Services Account. + +* `identity` - An `identity` block as defined below. + +* `primary_access_key` - A primary access key which can be used to connect to the AI Services Account. + +* `secondary_access_key` - The secondary access key which can be used to connect to the AI Services Account. + +--- + +An `identity` block exports the following: + +* `principal_id` - The Principal ID associated with this Managed Service Identity. + +* `tenant_id` - The Tenant ID associated with this Managed Service Identity. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the AI Services Account. +* `update` - (Defaults to 30 minutes) Used when updating the AI Services Account. +* `read` - (Defaults to 5 minutes) Used when retrieving the AI Services Account. +* `delete` - (Defaults to 30 minutes) Used when deleting the AI Services Account. + +## Import + +AI Services Account can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_ai_services.account1 /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.CognitiveServices/accounts/account1 +```