From 118e07a480d6d2afa77bd0ca2faaeee5071fcfaa Mon Sep 17 00:00:00 2001 From: The Magician Date: Wed, 27 Mar 2024 14:45:00 -0700 Subject: [PATCH] feat(vertexai): Support google_vertex_ai_deployment_resource_pool (#9518) (#17707) [upstream:83d97a1cc2fff05febc6c1616e5e140d5c3c3285] Signed-off-by: Modular Magician --- .changelog/9518.txt | 3 + google/provider/provider_mmv1_resources.go | 5 +- ...urce_vertex_ai_deployment_resource_pool.go | 664 ++++++++++++++++++ ...deployment_resource_pool_generated_test.go | 119 ++++ ...tex_ai_deployment_resource_pool_sweeper.go | 139 ++++ ..._ai_deployment_resource_pool.html.markdown | 181 +++++ 6 files changed, 1109 insertions(+), 2 deletions(-) create mode 100644 .changelog/9518.txt create mode 100644 google/services/vertexai/resource_vertex_ai_deployment_resource_pool.go create mode 100644 google/services/vertexai/resource_vertex_ai_deployment_resource_pool_generated_test.go create mode 100644 google/services/vertexai/resource_vertex_ai_deployment_resource_pool_sweeper.go create mode 100644 website/docs/r/vertex_ai_deployment_resource_pool.html.markdown diff --git a/.changelog/9518.txt b/.changelog/9518.txt new file mode 100644 index 00000000000..46a9c656254 --- /dev/null +++ b/.changelog/9518.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +`google_vertex_ai_deployment_resource_pool` +``` \ No newline at end of file diff --git a/google/provider/provider_mmv1_resources.go b/google/provider/provider_mmv1_resources.go index 2eddeeea320..c7eea8678de 100644 --- a/google/provider/provider_mmv1_resources.go +++ b/google/provider/provider_mmv1_resources.go @@ -397,9 +397,9 @@ var handwrittenIAMDatasources = map[string]*schema.Resource{ } // Resources -// Generated resources: 397 +// Generated resources: 398 // Generated IAM resources: 234 -// Total generated resources: 631 +// Total generated resources: 632 var generatedResources = map[string]*schema.Resource{ "google_folder_access_approval_settings": accessapproval.ResourceAccessApprovalFolderSettings(), "google_organization_access_approval_settings": accessapproval.ResourceAccessApprovalOrganizationSettings(), @@ -989,6 +989,7 @@ var generatedResources = map[string]*schema.Resource{ "google_tags_tag_value_iam_policy": tpgiamresource.ResourceIamPolicy(tags.TagsTagValueIamSchema, tags.TagsTagValueIamUpdaterProducer, tags.TagsTagValueIdParseFunc), "google_tpu_node": tpu.ResourceTPUNode(), "google_vertex_ai_dataset": vertexai.ResourceVertexAIDataset(), + "google_vertex_ai_deployment_resource_pool": vertexai.ResourceVertexAIDeploymentResourcePool(), "google_vertex_ai_endpoint": vertexai.ResourceVertexAIEndpoint(), "google_vertex_ai_feature_group": vertexai.ResourceVertexAIFeatureGroup(), "google_vertex_ai_feature_group_feature": vertexai.ResourceVertexAIFeatureGroupFeature(), diff --git a/google/services/vertexai/resource_vertex_ai_deployment_resource_pool.go b/google/services/vertexai/resource_vertex_ai_deployment_resource_pool.go new file mode 100644 index 00000000000..9c047884d42 --- /dev/null +++ b/google/services/vertexai/resource_vertex_ai_deployment_resource_pool.go @@ -0,0 +1,664 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package vertexai + +import ( + "fmt" + "log" + "reflect" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func ResourceVertexAIDeploymentResourcePool() *schema.Resource { + return &schema.Resource{ + Create: resourceVertexAIDeploymentResourcePoolCreate, + Read: resourceVertexAIDeploymentResourcePoolRead, + Delete: resourceVertexAIDeploymentResourcePoolDelete, + + Importer: &schema.ResourceImporter{ + State: resourceVertexAIDeploymentResourcePoolImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(20 * time.Minute), + Delete: schema.DefaultTimeout(20 * time.Minute), + }, + + CustomizeDiff: customdiff.All( + tpgresource.DefaultProviderProject, + ), + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The resource name of deployment resource pool. The maximum length is 63 characters, and valid characters are '/^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/'.`, + }, + "dedicated_resources": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: `The underlying dedicated resources that the deployment resource pool uses.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "machine_spec": { + Type: schema.TypeList, + Required: true, + ForceNew: true, + Description: `The specification of a single machine used by the prediction`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "accelerator_count": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: `The number of accelerators to attach to the machine.`, + }, + "accelerator_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `The type of accelerator(s) that may be attached to the machine as per accelerator_count. See possible values [here](https://cloud.google.com/vertex-ai/docs/reference/rest/v1/MachineSpec#AcceleratorType).`, + }, + "machine_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `The type of the machine. See the [list of machine types supported for prediction](https://cloud.google.com/vertex-ai/docs/predictions/configure-compute#machine-types).`, + }, + }, + }, + }, + "min_replica_count": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: `The minimum number of machine replicas this DeployedModel will be always deployed on. This value must be greater than or equal to 1. If traffic against the DeployedModel increases, it may dynamically be deployed onto more replicas, and as traffic decreases, some of these extra replicas may be freed.`, + }, + "autoscaling_metric_specs": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: `A list of the metric specifications that overrides a resource utilization metric.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The resource metric name. Supported metrics: For Online Prediction: * 'aiplatform.googleapis.com/prediction/online/accelerator/duty_cycle' * 'aiplatform.googleapis.com/prediction/online/cpu/utilization'`, + }, + "target": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: `The target resource utilization in percentage (1% - 100%) for the given metric; once the real usage deviates from the target by a certain percentage, the machine replicas change. The default value is 60 (representing 60%) if not provided.`, + }, + }, + }, + }, + "max_replica_count": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: `The maximum number of replicas this DeployedModel may be deployed on when the traffic against it increases. If the requested value is too large, the deployment will error, but if deployment succeeds then the ability to scale the model to that many replicas is guaranteed (barring service outages). If traffic against the DeployedModel increases beyond what its replicas at maximum may handle, a portion of the traffic will be dropped. If this value is not provided, will use min_replica_count as the default value. The value of this field impacts the charge against Vertex CPU and GPU quotas. Specifically, you will be charged for max_replica_count * number of cores in the selected machine type) and (max_replica_count * number of GPUs per replica in the selected machine type).`, + }, + }, + }, + }, + "region": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `The region of deployment resource pool. eg us-central1`, + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: `A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional digits.`, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + UseJSONNumber: true, + } +} + +func resourceVertexAIDeploymentResourcePoolCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + nameProp, err := expandVertexAIDeploymentResourcePoolName(d.Get("name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("name"); !tpgresource.IsEmptyValue(reflect.ValueOf(nameProp)) && (ok || !reflect.DeepEqual(v, nameProp)) { + obj["name"] = nameProp + } + dedicatedResourcesProp, err := expandVertexAIDeploymentResourcePoolDedicatedResources(d.Get("dedicated_resources"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("dedicated_resources"); !tpgresource.IsEmptyValue(reflect.ValueOf(dedicatedResourcesProp)) && (ok || !reflect.DeepEqual(v, dedicatedResourcesProp)) { + obj["dedicatedResources"] = dedicatedResourcesProp + } + + obj, err = resourceVertexAIDeploymentResourcePoolEncoder(d, meta, obj) + if err != nil { + return err + } + + url, err := tpgresource.ReplaceVars(d, config, "{{VertexAIBasePath}}projects/{{project}}/locations/{{region}}/deploymentResourcePools") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new DeploymentResourcePool: %#v", obj) + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for DeploymentResourcePool: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutCreate), + }) + if err != nil { + return fmt.Errorf("Error creating DeploymentResourcePool: %s", err) + } + + // Store the ID now + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{region}}/deploymentResourcePools/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + // Use the resource in the operation response to populate + // identity fields and d.Id() before read + var opRes map[string]interface{} + err = VertexAIOperationWaitTimeWithResponse( + config, res, &opRes, project, "Creating DeploymentResourcePool", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + // The resource didn't actually create + d.SetId("") + + return fmt.Errorf("Error waiting to create DeploymentResourcePool: %s", err) + } + + if err := d.Set("name", flattenVertexAIDeploymentResourcePoolName(opRes["name"], d, config)); err != nil { + return err + } + + // This may have caused the ID to update - update it if so. + id, err = tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{region}}/deploymentResourcePools/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished creating DeploymentResourcePool %q: %#v", d.Id(), res) + + return resourceVertexAIDeploymentResourcePoolRead(d, meta) +} + +func resourceVertexAIDeploymentResourcePoolRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + url, err := tpgresource.ReplaceVars(d, config, "{{VertexAIBasePath}}projects/{{project}}/locations/{{region}}/deploymentResourcePools/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for DeploymentResourcePool: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("VertexAIDeploymentResourcePool %q", d.Id())) + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading DeploymentResourcePool: %s", err) + } + + if err := d.Set("name", flattenVertexAIDeploymentResourcePoolName(res["name"], d, config)); err != nil { + return fmt.Errorf("Error reading DeploymentResourcePool: %s", err) + } + if err := d.Set("dedicated_resources", flattenVertexAIDeploymentResourcePoolDedicatedResources(res["dedicatedResources"], d, config)); err != nil { + return fmt.Errorf("Error reading DeploymentResourcePool: %s", err) + } + if err := d.Set("create_time", flattenVertexAIDeploymentResourcePoolCreateTime(res["createTime"], d, config)); err != nil { + return fmt.Errorf("Error reading DeploymentResourcePool: %s", err) + } + + return nil +} + +func resourceVertexAIDeploymentResourcePoolDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for DeploymentResourcePool: %s", err) + } + billingProject = project + + url, err := tpgresource.ReplaceVars(d, config, "{{VertexAIBasePath}}projects/{{project}}/locations/{{region}}/deploymentResourcePools/{{name}}") + if err != nil { + return err + } + + var obj map[string]interface{} + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + log.Printf("[DEBUG] Deleting DeploymentResourcePool %q", d.Id()) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "DELETE", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutDelete), + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, "DeploymentResourcePool") + } + + err = VertexAIOperationWaitTime( + config, res, project, "Deleting DeploymentResourcePool", userAgent, + d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Finished deleting DeploymentResourcePool %q: %#v", d.Id(), res) + return nil +} + +func resourceVertexAIDeploymentResourcePoolImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*transport_tpg.Config) + if err := tpgresource.ParseImportId([]string{ + "^projects/(?P[^/]+)/locations/(?P[^/]+)/deploymentResourcePools/(?P[^/]+)$", + "^(?P[^/]+)/(?P[^/]+)/(?P[^/]+)$", + "^(?P[^/]+)/(?P[^/]+)$", + "^(?P[^/]+)$", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{region}}/deploymentResourcePools/{{name}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + return []*schema.ResourceData{d}, nil +} + +func flattenVertexAIDeploymentResourcePoolName(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + return tpgresource.NameFromSelfLinkStateFunc(v) +} + +func flattenVertexAIDeploymentResourcePoolDedicatedResources(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["machine_spec"] = + flattenVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpec(original["machineSpec"], d, config) + transformed["min_replica_count"] = + flattenVertexAIDeploymentResourcePoolDedicatedResourcesMinReplicaCount(original["minReplicaCount"], d, config) + transformed["max_replica_count"] = + flattenVertexAIDeploymentResourcePoolDedicatedResourcesMaxReplicaCount(original["maxReplicaCount"], d, config) + transformed["autoscaling_metric_specs"] = + flattenVertexAIDeploymentResourcePoolDedicatedResourcesAutoscalingMetricSpecs(original["autoscalingMetricSpecs"], d, config) + return []interface{}{transformed} +} +func flattenVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpec(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["machine_type"] = + flattenVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpecMachineType(original["machineType"], d, config) + transformed["accelerator_type"] = + flattenVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpecAcceleratorType(original["acceleratorType"], d, config) + transformed["accelerator_count"] = + flattenVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpecAcceleratorCount(original["acceleratorCount"], d, config) + return []interface{}{transformed} +} +func flattenVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpecMachineType(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpecAcceleratorType(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpecAcceleratorCount(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := tpgresource.StringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func flattenVertexAIDeploymentResourcePoolDedicatedResourcesMinReplicaCount(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := tpgresource.StringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func flattenVertexAIDeploymentResourcePoolDedicatedResourcesMaxReplicaCount(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := tpgresource.StringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func flattenVertexAIDeploymentResourcePoolDedicatedResourcesAutoscalingMetricSpecs(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := make([]interface{}, 0, len(l)) + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + transformed = append(transformed, map[string]interface{}{ + "metric_name": flattenVertexAIDeploymentResourcePoolDedicatedResourcesAutoscalingMetricSpecsMetricName(original["metricName"], d, config), + "target": flattenVertexAIDeploymentResourcePoolDedicatedResourcesAutoscalingMetricSpecsTarget(original["target"], d, config), + }) + } + return transformed +} +func flattenVertexAIDeploymentResourcePoolDedicatedResourcesAutoscalingMetricSpecsMetricName(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenVertexAIDeploymentResourcePoolDedicatedResourcesAutoscalingMetricSpecsTarget(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := tpgresource.StringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func flattenVertexAIDeploymentResourcePoolCreateTime(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func expandVertexAIDeploymentResourcePoolName(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandVertexAIDeploymentResourcePoolDedicatedResources(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedMachineSpec, err := expandVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpec(original["machine_spec"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMachineSpec); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["machineSpec"] = transformedMachineSpec + } + + transformedMinReplicaCount, err := expandVertexAIDeploymentResourcePoolDedicatedResourcesMinReplicaCount(original["min_replica_count"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMinReplicaCount); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["minReplicaCount"] = transformedMinReplicaCount + } + + transformedMaxReplicaCount, err := expandVertexAIDeploymentResourcePoolDedicatedResourcesMaxReplicaCount(original["max_replica_count"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMaxReplicaCount); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["maxReplicaCount"] = transformedMaxReplicaCount + } + + transformedAutoscalingMetricSpecs, err := expandVertexAIDeploymentResourcePoolDedicatedResourcesAutoscalingMetricSpecs(original["autoscaling_metric_specs"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedAutoscalingMetricSpecs); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["autoscalingMetricSpecs"] = transformedAutoscalingMetricSpecs + } + + return transformed, nil +} + +func expandVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpec(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedMachineType, err := expandVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpecMachineType(original["machine_type"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMachineType); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["machineType"] = transformedMachineType + } + + transformedAcceleratorType, err := expandVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpecAcceleratorType(original["accelerator_type"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedAcceleratorType); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["acceleratorType"] = transformedAcceleratorType + } + + transformedAcceleratorCount, err := expandVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpecAcceleratorCount(original["accelerator_count"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedAcceleratorCount); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["acceleratorCount"] = transformedAcceleratorCount + } + + return transformed, nil +} + +func expandVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpecMachineType(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpecAcceleratorType(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandVertexAIDeploymentResourcePoolDedicatedResourcesMachineSpecAcceleratorCount(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandVertexAIDeploymentResourcePoolDedicatedResourcesMinReplicaCount(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandVertexAIDeploymentResourcePoolDedicatedResourcesMaxReplicaCount(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandVertexAIDeploymentResourcePoolDedicatedResourcesAutoscalingMetricSpecs(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + l := v.([]interface{}) + req := make([]interface{}, 0, len(l)) + for _, raw := range l { + if raw == nil { + continue + } + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedMetricName, err := expandVertexAIDeploymentResourcePoolDedicatedResourcesAutoscalingMetricSpecsMetricName(original["metric_name"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMetricName); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["metricName"] = transformedMetricName + } + + transformedTarget, err := expandVertexAIDeploymentResourcePoolDedicatedResourcesAutoscalingMetricSpecsTarget(original["target"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedTarget); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["target"] = transformedTarget + } + + req = append(req, transformed) + } + return req, nil +} + +func expandVertexAIDeploymentResourcePoolDedicatedResourcesAutoscalingMetricSpecsMetricName(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandVertexAIDeploymentResourcePoolDedicatedResourcesAutoscalingMetricSpecsTarget(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func resourceVertexAIDeploymentResourcePoolEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { + newObj := make(map[string]interface{}) + newObj["deploymentResourcePool"] = obj + nameProp, ok := d.GetOk("name") + if ok && nameProp != nil { + newObj["deploymentResourcePoolId"] = nameProp + } + return newObj, nil +} diff --git a/google/services/vertexai/resource_vertex_ai_deployment_resource_pool_generated_test.go b/google/services/vertexai/resource_vertex_ai_deployment_resource_pool_generated_test.go new file mode 100644 index 00000000000..8c05f9c6e0e --- /dev/null +++ b/google/services/vertexai/resource_vertex_ai_deployment_resource_pool_generated_test.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package vertexai_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func TestAccVertexAIDeploymentResourcePool_vertexAiDeploymentResourcePoolExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckVertexAIDeploymentResourcePoolDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccVertexAIDeploymentResourcePool_vertexAiDeploymentResourcePoolExample(context), + }, + { + ResourceName: "google_vertex_ai_deployment_resource_pool.deployment_resource_pool", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"region"}, + }, + }, + }) +} + +func testAccVertexAIDeploymentResourcePool_vertexAiDeploymentResourcePoolExample(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_vertex_ai_deployment_resource_pool" "deployment_resource_pool" { + region = "us-central1" + name = "tf-test-example-deployment-resource-pool%{random_suffix}" + dedicated_resources { + machine_spec { + machine_type = "n1-standard-4" + accelerator_type = "NVIDIA_TESLA_K80" + accelerator_count = 1 + } + + min_replica_count = 1 + max_replica_count = 2 + + autoscaling_metric_specs { + metric_name = "aiplatform.googleapis.com/prediction/online/accelerator/duty_cycle" + target = 60 + } + } +} +`, context) +} + +func testAccCheckVertexAIDeploymentResourcePoolDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_vertex_ai_deployment_resource_pool" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := acctest.GoogleProviderConfig(t) + + url, err := tpgresource.ReplaceVarsForTest(config, rs, "{{VertexAIBasePath}}projects/{{project}}/locations/{{region}}/deploymentResourcePools/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + if config.BillingProject != "" { + billingProject = config.BillingProject + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: config.UserAgent, + }) + if err == nil { + return fmt.Errorf("VertexAIDeploymentResourcePool still exists at %s", url) + } + } + + return nil + } +} diff --git a/google/services/vertexai/resource_vertex_ai_deployment_resource_pool_sweeper.go b/google/services/vertexai/resource_vertex_ai_deployment_resource_pool_sweeper.go new file mode 100644 index 00000000000..3fe1e13286e --- /dev/null +++ b/google/services/vertexai/resource_vertex_ai_deployment_resource_pool_sweeper.go @@ -0,0 +1,139 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package vertexai + +import ( + "context" + "log" + "strings" + "testing" + + "github.com/hashicorp/terraform-provider-google/google/envvar" + "github.com/hashicorp/terraform-provider-google/google/sweeper" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func init() { + sweeper.AddTestSweepers("VertexAIDeploymentResourcePool", testSweepVertexAIDeploymentResourcePool) +} + +// At the time of writing, the CI only passes us-central1 as the region +func testSweepVertexAIDeploymentResourcePool(region string) error { + resourceName := "VertexAIDeploymentResourcePool" + log.Printf("[INFO][SWEEPER_LOG] Starting sweeper for %s", resourceName) + + config, err := sweeper.SharedConfigForRegion(region) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error getting shared config for region: %s", err) + return err + } + + err = config.LoadAndValidate(context.Background()) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error loading: %s", err) + return err + } + + t := &testing.T{} + billingId := envvar.GetTestBillingAccountFromEnv(t) + + // Setup variables to replace in list template + d := &tpgresource.ResourceDataMock{ + FieldsInSchema: map[string]interface{}{ + "project": config.Project, + "region": region, + "location": region, + "zone": "-", + "billing_account": billingId, + }, + } + + listTemplate := strings.Split("https://{{region}}-aiplatform.googleapis.com/v1/projects/{{project}}/locations/{{region}}/deploymentResourcePools", "?")[0] + listUrl, err := tpgresource.ReplaceVars(d, config, listTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing sweeper list url: %s", err) + return nil + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: config.Project, + RawURL: listUrl, + UserAgent: config.UserAgent, + }) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error in response from request %s: %s", listUrl, err) + return nil + } + + resourceList, ok := res["deploymentResourcePools"] + if !ok { + log.Printf("[INFO][SWEEPER_LOG] Nothing found in response.") + return nil + } + + rl := resourceList.([]interface{}) + + log.Printf("[INFO][SWEEPER_LOG] Found %d items in %s list response.", len(rl), resourceName) + // Keep count of items that aren't sweepable for logging. + nonPrefixCount := 0 + for _, ri := range rl { + obj := ri.(map[string]interface{}) + if obj["name"] == nil { + log.Printf("[INFO][SWEEPER_LOG] %s resource name was nil", resourceName) + return nil + } + + name := tpgresource.GetResourceNameFromSelfLink(obj["name"].(string)) + // Skip resources that shouldn't be sweeped + if !sweeper.IsSweepableTestResource(name) { + nonPrefixCount++ + continue + } + + deleteTemplate := "https://{{region}}-aiplatform.googleapis.com/v1/projects/{{project}}/locations/{{region}}/deploymentResourcePools/{{name}}" + deleteUrl, err := tpgresource.ReplaceVars(d, config, deleteTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing delete url: %s", err) + return nil + } + deleteUrl = deleteUrl + name + + // Don't wait on operations as we may have a lot to delete + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "DELETE", + Project: config.Project, + RawURL: deleteUrl, + UserAgent: config.UserAgent, + }) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error deleting for url %s : %s", deleteUrl, err) + } else { + log.Printf("[INFO][SWEEPER_LOG] Sent delete request for %s resource: %s", resourceName, name) + } + } + + if nonPrefixCount > 0 { + log.Printf("[INFO][SWEEPER_LOG] %d items were non-sweepable and skipped.", nonPrefixCount) + } + + return nil +} diff --git a/website/docs/r/vertex_ai_deployment_resource_pool.html.markdown b/website/docs/r/vertex_ai_deployment_resource_pool.html.markdown new file mode 100644 index 00000000000..a5f878813d3 --- /dev/null +++ b/website/docs/r/vertex_ai_deployment_resource_pool.html.markdown @@ -0,0 +1,181 @@ +--- +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** Type: MMv1 *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in +# .github/CONTRIBUTING.md. +# +# ---------------------------------------------------------------------------- +subcategory: "Vertex AI" +description: |- + 'DeploymentResourcePool can be shared by multiple deployed models, + whose underlying specification consists of dedicated resources. +--- + +# google\_vertex\_ai\_deployment\_resource\_pool + +'DeploymentResourcePool can be shared by multiple deployed models, +whose underlying specification consists of dedicated resources.' + + +To get more information about DeploymentResourcePool, see: + +* [API documentation](https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.deploymentResourcePools) + + +## Example Usage - Vertex Ai Deployment Resource Pool + + +```hcl +resource "google_vertex_ai_deployment_resource_pool" "deployment_resource_pool" { + region = "us-central1" + name = "example-deployment-resource-pool" + dedicated_resources { + machine_spec { + machine_type = "n1-standard-4" + accelerator_type = "NVIDIA_TESLA_K80" + accelerator_count = 1 + } + + min_replica_count = 1 + max_replica_count = 2 + + autoscaling_metric_specs { + metric_name = "aiplatform.googleapis.com/prediction/online/accelerator/duty_cycle" + target = 60 + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + + +* `name` - + (Required) + The resource name of deployment resource pool. The maximum length is 63 characters, and valid characters are `/^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/`. + + +- - - + + +* `dedicated_resources` - + (Optional) + The underlying dedicated resources that the deployment resource pool uses. + Structure is [documented below](#nested_dedicated_resources). + +* `region` - + (Optional) + The region of deployment resource pool. eg us-central1 + +* `project` - (Optional) The ID of the project in which the resource belongs. + If it is not provided, the provider project is used. + + +The `dedicated_resources` block supports: + +* `machine_spec` - + (Required) + The specification of a single machine used by the prediction + Structure is [documented below](#nested_machine_spec). + +* `min_replica_count` - + (Required) + The minimum number of machine replicas this DeployedModel will be always deployed on. This value must be greater than or equal to 1. If traffic against the DeployedModel increases, it may dynamically be deployed onto more replicas, and as traffic decreases, some of these extra replicas may be freed. + +* `max_replica_count` - + (Optional) + The maximum number of replicas this DeployedModel may be deployed on when the traffic against it increases. If the requested value is too large, the deployment will error, but if deployment succeeds then the ability to scale the model to that many replicas is guaranteed (barring service outages). If traffic against the DeployedModel increases beyond what its replicas at maximum may handle, a portion of the traffic will be dropped. If this value is not provided, will use min_replica_count as the default value. The value of this field impacts the charge against Vertex CPU and GPU quotas. Specifically, you will be charged for max_replica_count * number of cores in the selected machine type) and (max_replica_count * number of GPUs per replica in the selected machine type). + +* `autoscaling_metric_specs` - + (Optional) + A list of the metric specifications that overrides a resource utilization metric. + Structure is [documented below](#nested_autoscaling_metric_specs). + + +The `machine_spec` block supports: + +* `machine_type` - + (Optional) + The type of the machine. See the [list of machine types supported for prediction](https://cloud.google.com/vertex-ai/docs/predictions/configure-compute#machine-types). + +* `accelerator_type` - + (Optional) + The type of accelerator(s) that may be attached to the machine as per accelerator_count. See possible values [here](https://cloud.google.com/vertex-ai/docs/reference/rest/v1/MachineSpec#AcceleratorType). + +* `accelerator_count` - + (Optional) + The number of accelerators to attach to the machine. + +The `autoscaling_metric_specs` block supports: + +* `metric_name` - + (Required) + The resource metric name. Supported metrics: For Online Prediction: * `aiplatform.googleapis.com/prediction/online/accelerator/duty_cycle` * `aiplatform.googleapis.com/prediction/online/cpu/utilization` + +* `target` - + (Optional) + The target resource utilization in percentage (1% - 100%) for the given metric; once the real usage deviates from the target by a certain percentage, the machine replicas change. The default value is 60 (representing 60%) if not provided. + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +* `id` - an identifier for the resource with format `projects/{{project}}/locations/{{region}}/deploymentResourcePools/{{name}}` + +* `create_time` - + A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional digits. + + +## Timeouts + +This resource provides the following +[Timeouts](https://developer.hashicorp.com/terraform/plugin/sdkv2/resources/retries-and-customizable-timeouts) configuration options: + +- `create` - Default is 20 minutes. +- `delete` - Default is 20 minutes. + +## Import + + +DeploymentResourcePool can be imported using any of these accepted formats: + +* `projects/{{project}}/locations/{{region}}/deploymentResourcePools/{{name}}` +* `{{project}}/{{region}}/{{name}}` +* `{{region}}/{{name}}` +* `{{name}}` + + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import DeploymentResourcePool using one of the formats above. For example: + +```tf +import { + id = "projects/{{project}}/locations/{{region}}/deploymentResourcePools/{{name}}" + to = google_vertex_ai_deployment_resource_pool.default +} +``` + +When using the [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import), DeploymentResourcePool can be imported using one of the formats above. For example: + +``` +$ terraform import google_vertex_ai_deployment_resource_pool.default projects/{{project}}/locations/{{region}}/deploymentResourcePools/{{name}} +$ terraform import google_vertex_ai_deployment_resource_pool.default {{project}}/{{region}}/{{name}} +$ terraform import google_vertex_ai_deployment_resource_pool.default {{region}}/{{name}} +$ terraform import google_vertex_ai_deployment_resource_pool.default {{name}} +``` + +## User Project Overrides + +This resource supports [User Project Overrides](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference#user_project_override).