diff --git a/azurerm/internal/services/machinelearning/machine_learning_compute_cluster_resource.go b/azurerm/internal/services/machinelearning/machine_learning_compute_cluster_resource.go new file mode 100644 index 000000000000..c4fca8612b11 --- /dev/null +++ b/azurerm/internal/services/machinelearning/machine_learning_compute_cluster_resource.go @@ -0,0 +1,339 @@ +package machinelearning + +import ( + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/services/machinelearningservices/mgmt/2020-04-01/machinelearningservices" + "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/services/machinelearning/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/pluginsdk" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceComputeCluster() *pluginsdk.Resource { + return &pluginsdk.Resource{ + Create: resourceComputeClusterCreate, + Read: resourceComputeClusterRead, + Delete: resourceComputeClusterDelete, + + Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error { + _, err := parse.ComputeClusterID(id) + return err + }), + + Timeouts: &pluginsdk.ResourceTimeout{ + Create: pluginsdk.DefaultTimeout(30 * time.Minute), + Read: pluginsdk.DefaultTimeout(5 * time.Minute), + Delete: pluginsdk.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + + "machine_learning_workspace_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + + "location": azure.SchemaLocation(), + + "vm_size": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + + "vm_priority": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{string(machinelearningservices.Dedicated), string(machinelearningservices.LowPriority)}, false), + }, + + "identity": { + Type: pluginsdk.TypeList, + Required: true, + ForceNew: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "type": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + string(machinelearningservices.SystemAssigned), + }, false), + }, + "principal_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "tenant_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + }, + + "scale_settings": { + Type: pluginsdk.TypeList, + Required: true, + ForceNew: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "max_node_count": { + Type: pluginsdk.TypeInt, + Required: true, + ForceNew: true, + }, + "min_node_count": { + Type: pluginsdk.TypeInt, + Required: true, + ForceNew: true, + }, + "scale_down_nodes_after_idle_duration": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + }, + }, + }, + + "description": { + Type: pluginsdk.TypeString, + Optional: true, + ForceNew: true, + }, + + "subnet_resource_id": { + Type: pluginsdk.TypeString, + Optional: true, + ForceNew: true, + }, + + "tags": tags.ForceNewSchema(), + }, + } +} + +func resourceComputeClusterCreate(d *pluginsdk.ResourceData, meta interface{}) error { + mlWorkspacesClient := meta.(*clients.Client).MachineLearning.WorkspacesClient + mlComputeClient := meta.(*clients.Client).MachineLearning.MachineLearningComputeClient + ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + + // Get Machine Learning Workspace Name and Resource Group from ID + workspaceID, err := parse.WorkspaceID(d.Get("machine_learning_workspace_id").(string)) + if err != nil { + return err + } + + existing, err := mlComputeClient.Get(ctx, workspaceID.ResourceGroup, workspaceID.Name, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("error checking for existing Compute Cluster %q in Workspace %q (Resource Group %q): %s", + name, workspaceID.Name, workspaceID.ResourceGroup, err) + } + } + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_machine_learning_compute_cluster", *existing.ID) + } + + computeClusterProperties := machinelearningservices.AmlCompute{ + Properties: &machinelearningservices.AmlComputeProperties{ + VMSize: utils.String(d.Get("vm_size").(string)), + VMPriority: machinelearningservices.VMPriority(d.Get("vm_priority").(string)), + ScaleSettings: expandScaleSettings(d.Get("scale_settings").([]interface{})), + Subnet: &machinelearningservices.ResourceID{ID: utils.String(d.Get("subnet_resource_id").(string))}, + }, + ComputeLocation: utils.String(d.Get("location").(string)), + Description: utils.String(d.Get("description").(string)), + } + + amlComputeProperties, isAmlCompute := (machinelearningservices.BasicCompute).AsAmlCompute(computeClusterProperties) + if !isAmlCompute { + return fmt.Errorf("no compute cluster") + } + + // Get SKU from Workspace + workspace, err := mlWorkspacesClient.Get(ctx, workspaceID.ResourceGroup, workspaceID.Name) + if err != nil { + return err + } + + computeClusterParameters := machinelearningservices.ComputeResource{ + Properties: amlComputeProperties, + Identity: expandComputeClusterIdentity(d.Get("identity").([]interface{})), + Location: computeClusterProperties.ComputeLocation, + Tags: tags.Expand(d.Get("tags").(map[string]interface{})), + Sku: workspace.Sku, + } + + future, err := mlComputeClient.CreateOrUpdate(ctx, workspaceID.ResourceGroup, workspaceID.Name, name, computeClusterParameters) + if err != nil { + return fmt.Errorf("creating Compute Cluster %q in workspace %q (Resource Group %q): %+v", + name, workspaceID.Name, workspaceID.ResourceGroup, err) + } + if err := future.WaitForCompletionRef(ctx, mlComputeClient.Client); err != nil { + return fmt.Errorf("waiting for creation of Compute Cluster %q in workspace %q (Resource Group %q): %+v", + name, workspaceID.Name, workspaceID.ResourceGroup, err) + } + + subscriptionId := meta.(*clients.Client).Account.SubscriptionId + id := parse.NewComputeClusterID(subscriptionId, workspaceID.ResourceGroup, workspaceID.Name, name) + d.SetId(id.ID()) + + return resourceComputeClusterRead(d, meta) +} + +func resourceComputeClusterRead(d *pluginsdk.ResourceData, meta interface{}) error { + mlComputeClient := meta.(*clients.Client).MachineLearning.MachineLearningComputeClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.ComputeClusterID(d.Id()) + if err != nil { + return fmt.Errorf("parsing Compute Cluster ID `%q`: %+v", d.Id(), err) + } + + computeResource, err := mlComputeClient.Get(ctx, id.ResourceGroup, id.WorkspaceName, id.ComputeName) + if err != nil { + if utils.ResponseWasNotFound(computeResource.Response) { + d.SetId("") + return nil + } + return fmt.Errorf("making Read request on Compute Cluster %q in Workspace %q (Resource Group %q): %+v", + id.ComputeName, id.WorkspaceName, id.ResourceGroup, err) + } + + d.Set("name", id.ComputeName) + + subscriptionId := meta.(*clients.Client).Account.SubscriptionId + workspaceId := parse.NewWorkspaceID(subscriptionId, id.ResourceGroup, id.WorkspaceName) + d.Set("machine_learning_workspace_id", workspaceId.ID()) + + // use ComputeResource to get to AKS Cluster ID and other properties + computeCluster, isComputeCluster := (machinelearningservices.BasicCompute).AsAmlCompute(computeResource.Properties) + if !isComputeCluster { + return fmt.Errorf("compute resource %s is not an Aml Compute cluster", id.ComputeName) + } + + d.Set("vm_size", computeCluster.Properties.VMSize) + d.Set("vm_priority", computeCluster.Properties.VMPriority) + d.Set("scale_settings", flattenScaleSettings(computeCluster.Properties.ScaleSettings)) + d.Set("subnet_resource_id", computeCluster.Properties.Subnet.ID) + + if location := computeResource.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + + if err := d.Set("identity", flattenComputeClusterIdentity(computeResource.Identity)); err != nil { + return fmt.Errorf("flattening identity on Workspace %q (Resource Group %q): %+v", + id.ComputeName, id.ResourceGroup, err) + } + + return tags.FlattenAndSet(d, computeResource.Tags) +} + +func resourceComputeClusterDelete(d *pluginsdk.ResourceData, meta interface{}) error { + mlComputeClient := meta.(*clients.Client).MachineLearning.MachineLearningComputeClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + id, err := parse.ComputeClusterID(d.Id()) + if err != nil { + return fmt.Errorf("parsing Compute Cluster ID `%q`: %+v", d.Id(), err) + } + future, err := mlComputeClient.Delete(ctx, id.ResourceGroup, id.WorkspaceName, id.ComputeName, machinelearningservices.Detach) + if err != nil { + return fmt.Errorf("deleting Compute Cluster %q in workspace %q (Resource Group %q): %+v", id.ComputeName, id.WorkspaceName, id.ResourceGroup, err) + } + if err := future.WaitForCompletionRef(ctx, mlComputeClient.Client); err != nil { + return fmt.Errorf("waiting for deletion of Compute Cluster %q in workspace %q (Resource Group %q): %+v", id.ComputeName, id.WorkspaceName, id.ResourceGroup, err) + } + return nil +} + +func expandScaleSettings(input []interface{}) *machinelearningservices.ScaleSettings { + if len(input) == 0 { + return nil + } + + v := input[0].(map[string]interface{}) + + max_node_count := int32(v["max_node_count"].(int)) + min_node_count := int32(v["min_node_count"].(int)) + scale_down_nodes_after_idle_duration := v["scale_down_nodes_after_idle_duration"].(string) + + return &machinelearningservices.ScaleSettings{ + MaxNodeCount: &max_node_count, + MinNodeCount: &min_node_count, + NodeIdleTimeBeforeScaleDown: &scale_down_nodes_after_idle_duration, + } +} + +func flattenScaleSettings(scaleSettings *machinelearningservices.ScaleSettings) []interface{} { + if scaleSettings == nil { + return []interface{}{} + } + + return []interface{}{ + map[string]interface{}{ + "max_node_count": scaleSettings.MaxNodeCount, + "min_node_count": scaleSettings.MinNodeCount, + "scale_down_nodes_after_idle_duration": scaleSettings.NodeIdleTimeBeforeScaleDown, + }, + } +} + +func expandComputeClusterIdentity(input []interface{}) *machinelearningservices.Identity { + if len(input) == 0 { + return nil + } + + v := input[0].(map[string]interface{}) + + return &machinelearningservices.Identity{ + Type: machinelearningservices.ResourceIdentityType(v["type"].(string)), + } +} + +func flattenComputeClusterIdentity(identity *machinelearningservices.Identity) []interface{} { + if identity == nil { + return []interface{}{} + } + + principalID := "" + if identity.PrincipalID != nil { + principalID = *identity.PrincipalID + } + + tenantID := "" + if identity.TenantID != nil { + tenantID = *identity.TenantID + } + + return []interface{}{ + map[string]interface{}{ + "type": string(identity.Type), + "principal_id": principalID, + "tenant_id": tenantID, + }, + } +} diff --git a/azurerm/internal/services/machinelearning/machine_learning_compute_cluster_resource_test.go b/azurerm/internal/services/machinelearning/machine_learning_compute_cluster_resource_test.go new file mode 100644 index 000000000000..34167af6b531 --- /dev/null +++ b/azurerm/internal/services/machinelearning/machine_learning_compute_cluster_resource_test.go @@ -0,0 +1,205 @@ +package machinelearning_test + +import ( + "context" + "fmt" + "testing" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance/check" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/machinelearning/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/pluginsdk" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +type ComputeClusterResource struct{} + +func TestAccComputeCluster_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_machine_learning_compute_cluster", "test") + r := ComputeClusterResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("identity.#").HasValue("1"), + check.That(data.ResourceName).Key("identity.0.type").HasValue("SystemAssigned"), + check.That(data.ResourceName).Key("identity.0.principal_id").Exists(), + check.That(data.ResourceName).Key("identity.0.tenant_id").Exists(), + check.That(data.ResourceName).Key("scale_settings.#").HasValue("1"), + check.That(data.ResourceName).Key("scale_settings.0.max_node_count").Exists(), + check.That(data.ResourceName).Key("scale_settings.0.min_node_count").Exists(), + check.That(data.ResourceName).Key("scale_settings.0.scale_down_nodes_after_idle_duration").Exists(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccComputeCluster_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_machine_learning_compute_cluster", "test") + r := ComputeClusterResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("identity.#").HasValue("1"), + check.That(data.ResourceName).Key("identity.0.type").HasValue("SystemAssigned"), + check.That(data.ResourceName).Key("identity.0.principal_id").Exists(), + check.That(data.ResourceName).Key("identity.0.tenant_id").Exists(), + check.That(data.ResourceName).Key("scale_settings.#").HasValue("1"), + check.That(data.ResourceName).Key("scale_settings.0.max_node_count").Exists(), + check.That(data.ResourceName).Key("scale_settings.0.min_node_count").Exists(), + check.That(data.ResourceName).Key("scale_settings.0.scale_down_nodes_after_idle_duration").Exists(), + ), + }, + data.RequiresImportErrorStep(r.requiresImport), + }) +} + +func (r ComputeClusterResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + computeClusterClient := client.MachineLearning.MachineLearningComputeClient + id, err := parse.ComputeClusterID(state.ID) + + if err != nil { + return nil, err + } + + computeResource, err := computeClusterClient.Get(ctx, id.ResourceGroup, id.WorkspaceName, id.ComputeName) + if err != nil { + if utils.ResponseWasNotFound(computeResource.Response) { + return utils.Bool(false), nil + } + return nil, fmt.Errorf("retrieving Machine Learning Compute Cluster %q: %+v", state.ID, err) + } + return utils.Bool(computeResource.Properties != nil), nil +} + +func (r ComputeClusterResource) basic(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_machine_learning_compute_cluster" "test" { + name = "CC-%d" + location = azurerm_resource_group.test.location + vm_priority = "LowPriority" + vm_size = "STANDARD_DS2_V2" + machine_learning_workspace_id = azurerm_machine_learning_workspace.test.id + subnet_resource_id = azurerm_subnet.test.id + + scale_settings { + min_node_count = 0 + max_node_count = 1 + scale_down_nodes_after_idle_duration = "PT30S" # 30 seconds + } + + identity { + type = "SystemAssigned" + } +} +`, template, data.RandomIntOfLength(8)) +} + +func (r ComputeClusterResource) requiresImport(data acceptance.TestData) string { + template := r.basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_machine_learning_compute_cluster" "import" { + name = azurerm_machine_learning_compute_cluster.test.name + location = azurerm_machine_learning_compute_cluster.test.location + vm_priority = azurerm_machine_learning_compute_cluster.test.vm_priority + vm_size = azurerm_machine_learning_compute_cluster.test.vm_size + machine_learning_workspace_id = azurerm_machine_learning_compute_cluster.test.machine_learning_workspace_id + + scale_settings { + min_node_count = 0 + max_node_count = 1 + scale_down_nodes_after_idle_duration = "PT2M" # 120 seconds + } + + identity { + type = "SystemAssigned" + } +} + +`, template) +} + +func (r ComputeClusterResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-ml-%[1]d" + location = "%[2]s" + tags = { + "stage" = "test" + } +} + +resource "azurerm_application_insights" "test" { + name = "acctestai-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + application_type = "web" +} + +resource "azurerm_key_vault" "test" { + name = "acctestvault%[3]d" + 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" + + purge_protection_enabled = true +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%[4]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_machine_learning_workspace" "test" { + name = "acctest-MLW%[5]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + application_insights_id = azurerm_application_insights.test.id + key_vault_id = azurerm_key_vault.test.id + storage_account_id = azurerm_storage_account.test.id + + identity { + type = "SystemAssigned" + } +} + +resource "azurerm_virtual_network" "test" { + name = "acctestvirtnet%[6]d" + address_space = ["10.1.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test" { + name = "acctestsubnet%[7]d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefix = "10.1.0.0/24" +} +`, data.RandomInteger, data.Locations.Primary, + data.RandomIntOfLength(12), data.RandomIntOfLength(15), data.RandomIntOfLength(16), + data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger) +} diff --git a/azurerm/internal/services/machinelearning/parse/compute_cluster.go b/azurerm/internal/services/machinelearning/parse/compute_cluster.go new file mode 100644 index 000000000000..4dcc2dd18144 --- /dev/null +++ b/azurerm/internal/services/machinelearning/parse/compute_cluster.go @@ -0,0 +1,75 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + "strings" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type ComputeClusterId struct { + SubscriptionId string + ResourceGroup string + WorkspaceName string + ComputeName string +} + +func NewComputeClusterID(subscriptionId, resourceGroup, workspaceName, computeName string) ComputeClusterId { + return ComputeClusterId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + WorkspaceName: workspaceName, + ComputeName: computeName, + } +} + +func (id ComputeClusterId) String() string { + segments := []string{ + fmt.Sprintf("Compute Name %q", id.ComputeName), + fmt.Sprintf("Workspace Name %q", id.WorkspaceName), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + } + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "Compute Cluster", segmentsStr) +} + +func (id ComputeClusterId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.MachineLearningServices/workspaces/%s/computes/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.WorkspaceName, id.ComputeName) +} + +// ComputeClusterID parses a ComputeCluster ID into an ComputeClusterId struct +func ComputeClusterID(input string) (*ComputeClusterId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := ComputeClusterId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + if resourceId.WorkspaceName, err = id.PopSegment("workspaces"); err != nil { + return nil, err + } + if resourceId.ComputeName, err = id.PopSegment("computes"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} diff --git a/azurerm/internal/services/machinelearning/parse/compute_cluster_test.go b/azurerm/internal/services/machinelearning/parse/compute_cluster_test.go new file mode 100644 index 000000000000..de390027ca48 --- /dev/null +++ b/azurerm/internal/services/machinelearning/parse/compute_cluster_test.go @@ -0,0 +1,128 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "testing" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/resourceid" +) + +var _ resourceid.Formatter = ComputeClusterId{} + +func TestComputeClusterIDFormatter(t *testing.T) { + actual := NewComputeClusterID("00000000-0000-0000-0000-000000000000", "resGroup1", "workspace1", "cluster1").ID() + expected := "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/workspace1/computes/cluster1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestComputeClusterID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *ComputeClusterId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Error: true, + }, + + { + // missing WorkspaceName + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/", + Error: true, + }, + + { + // missing value for WorkspaceName + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/", + Error: true, + }, + + { + // missing ComputeName + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/workspace1/", + Error: true, + }, + + { + // missing value for ComputeName + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/workspace1/computes/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/workspace1/computes/cluster1", + Expected: &ComputeClusterId{ + SubscriptionId: "00000000-0000-0000-0000-000000000000", + ResourceGroup: "resGroup1", + WorkspaceName: "workspace1", + ComputeName: "cluster1", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/00000000-0000-0000-0000-000000000000/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.MACHINELEARNINGSERVICES/WORKSPACES/WORKSPACE1/COMPUTES/CLUSTER1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := ComputeClusterID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.WorkspaceName != v.Expected.WorkspaceName { + t.Fatalf("Expected %q but got %q for WorkspaceName", v.Expected.WorkspaceName, actual.WorkspaceName) + } + if actual.ComputeName != v.Expected.ComputeName { + t.Fatalf("Expected %q but got %q for ComputeName", v.Expected.ComputeName, actual.ComputeName) + } + } +} diff --git a/azurerm/internal/services/machinelearning/registration.go b/azurerm/internal/services/machinelearning/registration.go index e8fcd46fdefb..5d2caba3af04 100644 --- a/azurerm/internal/services/machinelearning/registration.go +++ b/azurerm/internal/services/machinelearning/registration.go @@ -29,5 +29,6 @@ func (r Registration) SupportedResources() map[string]*pluginsdk.Resource { return map[string]*pluginsdk.Resource{ "azurerm_machine_learning_workspace": resourceMachineLearningWorkspace(), "azurerm_machine_learning_inference_cluster": resourceAksInferenceCluster(), + "azurerm_machine_learning_compute_cluster": resourceComputeCluster(), } } diff --git a/azurerm/internal/services/machinelearning/resourceids.go b/azurerm/internal/services/machinelearning/resourceids.go index 18ebbab752ba..05f1cce453cb 100644 --- a/azurerm/internal/services/machinelearning/resourceids.go +++ b/azurerm/internal/services/machinelearning/resourceids.go @@ -1,5 +1,6 @@ package machinelearning +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=ComputeCluster -id=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/workspace1/computes/cluster1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=InferenceCluster -id=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/workspace1/computes/cluster1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=KubernetesCluster -id=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ContainerService/managedClusters/cluster1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Workspace -id=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/workspace1 diff --git a/azurerm/internal/services/machinelearning/validate/compute_cluster_id.go b/azurerm/internal/services/machinelearning/validate/compute_cluster_id.go new file mode 100644 index 000000000000..45d84480db6d --- /dev/null +++ b/azurerm/internal/services/machinelearning/validate/compute_cluster_id.go @@ -0,0 +1,23 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/machinelearning/parse" +) + +func ComputeClusterID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := parse.ComputeClusterID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/azurerm/internal/services/machinelearning/validate/compute_cluster_id_test.go b/azurerm/internal/services/machinelearning/validate/compute_cluster_id_test.go new file mode 100644 index 000000000000..540d30dd9d62 --- /dev/null +++ b/azurerm/internal/services/machinelearning/validate/compute_cluster_id_test.go @@ -0,0 +1,88 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestComputeClusterID(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + + { + // empty + Input: "", + Valid: false, + }, + + { + // missing SubscriptionId + Input: "/", + Valid: false, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Valid: false, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/", + Valid: false, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Valid: false, + }, + + { + // missing WorkspaceName + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/", + Valid: false, + }, + + { + // missing value for WorkspaceName + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/", + Valid: false, + }, + + { + // missing ComputeName + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/workspace1/", + Valid: false, + }, + + { + // missing value for ComputeName + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/workspace1/computes/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/workspace1/computes/cluster1", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/00000000-0000-0000-0000-000000000000/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.MACHINELEARNINGSERVICES/WORKSPACES/WORKSPACE1/COMPUTES/CLUSTER1", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := ComputeClusterID(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/website/docs/r/machine_learning_compute_cluster.html.markdown b/website/docs/r/machine_learning_compute_cluster.html.markdown new file mode 100644 index 000000000000..910e739eb696 --- /dev/null +++ b/website/docs/r/machine_learning_compute_cluster.html.markdown @@ -0,0 +1,161 @@ +--- +subcategory: "Machine Learning" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_machine_learning_compute_cluster" +description: |- + Manages a Machine Learning Compute Cluster. +--- + +# azurerm_machine_learning_compute_cluster + +Manages a Machine Learning Compute Cluster. +**NOTE:** At this point in time the resource cannot be updated (not supported by the backend Azure Go SDK). Therefore it can only be created and deleted, not updated. At the moment, there is also no possibility to specify ssh User Account Credentials to ssh into the compute cluster. + +## Example Usage + +```hcl +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "example" { + name = "example-rg" + location = "west europe" + tags = { + "stage" = "example" + } +} + +resource "azurerm_application_insights" "example" { + name = "example-ai" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + application_type = "web" +} + +resource "azurerm_key_vault" "example" { + name = "example-kv" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + tenant_id = data.azurerm_client_config.current.tenant_id + + sku_name = "standard" + + purge_protection_enabled = true +} + +resource "azurerm_storage_account" "example" { + name = "examplesa" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_machine_learning_workspace" "example" { + name = "example-mlw" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + application_insights_id = azurerm_application_insights.example.id + key_vault_id = azurerm_key_vault.example.id + storage_account_id = azurerm_storage_account.example.id + + identity { + type = "SystemAssigned" + } +} + +resource "azurerm_virtual_network" "example" { + name = "example-vnet" + address_space = ["10.1.0.0/16"] + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name +} + +resource "azurerm_subnet" "example" { + name = "example-subnet" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.example.name + address_prefixes = ["10.1.0.0/24"] +} + +resource "azurerm_machine_learning_compute_cluster" "test" { + name = "example" + location = "West Europe" + vm_priority = "LowPriority" + vm_size = "Standard_DS2_v2" + machine_learning_workspace_id = azurerm_machine_learning_workspace.example.id + subnet_resource_id = azurerm_subnet.example.id + + scale_settings { + min_node_count = 0 + max_node_count = 1 + node_idle_time_before_scale_down = "PT30S" # 30 seconds + } + + identity { + type = "SystemAssigned" + } +} +``` + +## Arguments Reference + +The following arguments are supported: +* `name` - (Required) The name which should be used for this Machine Learning Compute Cluster. Changing this forces a new Machine Learning Compute Cluster to be created. + +* `machine_learning_workspace_id` - (Required) The ID of the Machine Learning Workspace. Changing this forces a new Machine Learning Compute Cluster to be created. + +* `location` - (Required) The Azure Region where the Machine Learning Compute Cluster should exist. Changing this forces a new Machine Learning Compute Cluster to be created. + +* `vm_priority` - (Required) The priority of the VM. Changing this forces a new Machine Learning Compute Cluster to be created. + +* `vm_size` - (Required) The size of the VM. Changing this forces a new Machine Learning Compute Cluster to be created. + +* `identity` - (Required) A `identity` block as defined below. Changing this forces a new Machine Learning Compute Cluster to be created. + +* `scale_settings` - (Required) A `scale_settings` block as defined below. Changing this forces a new Machine Learning Compute Cluster to be created. + +--- + +* `description` - (Optional) The description of the Machine Learning compute. Changing this forces a new Machine Learning Compute Cluster to be created. + +* `subnet_resource_id` - (Optional) The ID of the Subnet that the Compute Cluster should reside in. Changing this forces a new Machine Learning Compute Cluster to be created. + +* `tags` - (Optional) A mapping of tags which should be assigned to the Machine Learning Compute Cluster. Changing this forces a new Machine Learning Compute Cluster to be created. + +--- + +A `identity` block supports the following: + +* `type` - (Required) The Type of Identity which should be used for this Disk Encryption Set. At this time the only possible value is SystemAssigned. Changing this forces a new Machine Learning Compute Cluster to be created. + +--- + +A `scale_settings` block supports the following: + +* `max_node_count` - (Required) Maximum node count. Changing this forces a new Machine Learning Compute Cluster to be created. + +* `min_node_count` - (Required) Minimal node count. Changing this forces a new Machine Learning Compute Cluster to be created. + +* `node_idle_time_before_scale_down` - (Required) Node Idle Time Before Scale Down: defines the time until the compute is shutdown when it has gone into Idle state. Is defined according to W3C XML schema standard for duration. Changing this forces a new Machine Learning Compute Cluster to be created. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Machine Learning Compute Cluster. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Machine Learning Compute Cluster. +* `read` - (Defaults to 5 minutes) Used when retrieving the Machine Learning Compute Cluster. +* `delete` - (Defaults to 30 minutes) Used when deleting the Machine Learning Compute Cluster. + +## Import + +Machine Learning Compute Clusters can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_machine_learning_compute_cluster.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.MachineLearningServices/workspaces/workspace1/computes/cluster1 +```