From ccd318b203a864c1ead051834b19362803443d1e Mon Sep 17 00:00:00 2001 From: wuzhuanhong Date: Thu, 26 Dec 2024 14:17:10 +0800 Subject: [PATCH] feat(modelarts): add resource to operate devserver --- docs/resources/modelarts_devserver_action.md | 44 ++++++ huaweicloud/provider.go | 1 + ...ce_huaweicloud_modelarts_devserver_test.go | 51 ++++++- ..._huaweicloud_modelarts_devserver_action.go | 135 ++++++++++++++++++ 4 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 docs/resources/modelarts_devserver_action.md create mode 100644 huaweicloud/services/modelarts/resource_huaweicloud_modelarts_devserver_action.go diff --git a/docs/resources/modelarts_devserver_action.md b/docs/resources/modelarts_devserver_action.md new file mode 100644 index 0000000000..49cef13c9d --- /dev/null +++ b/docs/resources/modelarts_devserver_action.md @@ -0,0 +1,44 @@ +--- +subcategory: "AI Development Platform (ModelArts)" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_modelarts_devserver_action" +description: |- + Use this resource to operate ModelArts DevServer within HuaweiCloud. +--- +# huaweicloud_modelarts_devserver_action + +Use this resource to operate ModelArts DevServer within HuaweiCloud. + +## Example Usage + +```hcl +variable "devserver_id" {} + +resource "huaweicloud_modelarts_devserver_action" "test" { + devserver_id = var.devserver_id + action = "start" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional, String, ForceNew) Specifies the region in which to create the resource. + If omitted, the provider-level region will be used. + Changing this creates a new resource. + +* `devserver_id` - (Required, String, ForceNew) Specifies the ID of the DevServer. + Changing this creates a new resource. + +* `action` - (Required, String, ForceNew) Specifies the action type of the DevServer. + Changing this creates a new resource. + The valid values are as follows: + + **start**: The DevServer can be started only when the DevServer is stopped, stop failure, or start failure. + + **stop**: The DevServer can be stopped only when it is running or stop failure. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The resource ID. diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index bfd6eb6ebb..22625207d4 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -1928,6 +1928,7 @@ func Provider() *schema.Provider { "huaweicloud_modelarts_dataset": modelarts.ResourceDataset(), "huaweicloud_modelarts_dataset_version": modelarts.ResourceDatasetVersion(), + "huaweicloud_modelarts_devserver_action": modelarts.ResourceDevServerAction(), "huaweicloud_modelarts_devserver": modelarts.ResourceDevServer(), "huaweicloud_modelarts_notebook": modelarts.ResourceNotebook(), "huaweicloud_modelarts_notebook_mount_storage": modelarts.ResourceNotebookMountStorage(), diff --git a/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_devserver_test.go b/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_devserver_test.go index 7147c51642..b9f993bb82 100644 --- a/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_devserver_test.go +++ b/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_devserver_test.go @@ -46,7 +46,7 @@ func TestAccDevServer_basic(t *testing.T) { CheckDestroy: rc.CheckResourceDestroy(), Steps: []resource.TestStep{ { - Config: testDevServer_basic(name, password, true), + Config: testAccDevServer_basic(true, name, password), Check: resource.ComposeTestCheckFunc( rc.CheckResourceExists(), resource.TestCheckResourceAttr(resourceName, "name", name), @@ -64,12 +64,30 @@ func TestAccDevServer_basic(t *testing.T) { ), }, { - Config: testDevServer_basic(name, password, false), + Config: testAccDevServer_basic(false, name, password), Check: resource.ComposeTestCheckFunc( rc.CheckResourceExists(), resource.TestCheckResourceAttr(resourceName, "auto_renew", "false"), ), }, + // Stopping the DevServer. + { + Config: testAccDevServer_doAction(name, password, "stop", false), + }, + // Stopping the stopped DevServer. + { + Config: testAccDevServer_doAction(name, password, "stop", true), + ExpectError: regexp.MustCompile(`Resource.Server '[a-f0-9-]+' is not allowed STOP: STOPPED`), + }, + // Starting the DevServer. + { + Config: testAccDevServer_doAction(name, password, "start", false), + }, + // Starting the running DevServer. + { + Config: testAccDevServer_doAction(name, password, "start", true), + ExpectError: regexp.MustCompile(`Resource.Server '[a-f0-9-]+' is not allowed START: RUNNING`), + }, { ResourceName: resourceName, ImportState: true, @@ -88,7 +106,7 @@ func TestAccDevServer_basic(t *testing.T) { }) } -func testDevServer_basic(name, password string, autoRenew bool) string { +func testAccDevServer_basic(isAutoRenew bool, name, password string) string { return fmt.Sprintf(` %[1]s @@ -112,5 +130,30 @@ resource "huaweicloud_modelarts_devserver" "test" { auto_renew = "%[6]v" } `, common.TestBaseNetwork(name), name, - acceptance.HW_MODELARTS_DEVSERVER_FLAVOR, acceptance.HW_MODELARTS_DEVSERVER_IMAGE_ID, password, autoRenew) + acceptance.HW_MODELARTS_DEVSERVER_FLAVOR, acceptance.HW_MODELARTS_DEVSERVER_IMAGE_ID, password, isAutoRenew) +} + +func testAccDevServer_doAction(name, password, actionType string, doRetryAction bool) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_modelarts_devserver_action" "test" { + devserver_id = huaweicloud_modelarts_devserver.test.id + action = "%[2]s" +} + +variable "is_retry_devserver_action" { + type = bool + default = "%[3]v" +} + +resource "huaweicloud_modelarts_devserver_action" "expect_err" { + count = var.is_retry_devserver_action ? 1 : 0 + + depends_on = [huaweicloud_modelarts_devserver_action.test] + + devserver_id = huaweicloud_modelarts_devserver.test.id + action = "%[2]s" +} +`, testAccDevServer_basic(false, name, password), actionType, doRetryAction) } diff --git a/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_devserver_action.go b/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_devserver_action.go new file mode 100644 index 0000000000..72f2239b9e --- /dev/null +++ b/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_devserver_action.go @@ -0,0 +1,135 @@ +package modelarts + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/chnsz/golangsdk" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils" +) + +// @API ModelArts PUT /v1/{project_id}/dev-servers/{id}/start +// @API ModelArts PUT /v1/{project_id}/dev-servers/{id}/stop +// @API ModelArts GET /v1/{project_id}/dev-servers/{id} +func ResourceDevServerAction() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDevServerActionCreate, + ReadContext: resourceDevServerActionRead, + DeleteContext: resourceDevServerActionDelete, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "devserver_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The ID of the DevServer.`, + }, + "action": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The action type of the DevServer.`, + }, + }, + } +} + +func resourceDevServerActionCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + httpUrl = "v1/{project_id}/dev-servers/{id}/{action}" + devServerId = d.Get("devserver_id").(string) + action = d.Get("action").(string) + actionCompletedMap = map[string]string{ + "start": "RUNNING", + "stop": "STOPPED", + } + ) + + client, err := cfg.NewServiceClient("modelarts", region) + if err != nil { + return diag.Errorf("error creating ModelArts client: %s", err) + } + + actionPath := client.Endpoint + httpUrl + actionPath = strings.ReplaceAll(actionPath, "{project_id}", client.ProjectID) + actionPath = strings.ReplaceAll(actionPath, "{id}", devServerId) + actionPath = strings.ReplaceAll(actionPath, "{action}", action) + actionOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + if action == "start" { + actionOpt.JSONBody = map[string]interface{}{} + } + + _, err = client.Request("PUT", actionPath, &actionOpt) + if err != nil { + return diag.Errorf("unable to %s DevServer (%s): %s", action, devServerId, err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING"}, + Target: []string{"COMPLETED"}, + Refresh: refreshDevServerActionStatusFunc(client, devServerId, actionCompletedMap[action]), + Timeout: d.Timeout(schema.TimeoutCreate), + Delay: 10 * time.Second, + PollInterval: 10 * time.Second, + } + _, err = stateConf.WaitForStateContext(ctx) + if err != nil { + return diag.Errorf("error waiting for DevServer (%s) to %s completed: %s", devServerId, action, err) + } + + d.SetId(devServerId) + + return nil +} + +func refreshDevServerActionStatusFunc(client *golangsdk.ServiceClient, devServerId string, target string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + respBody, err := GetDevServerById(client, devServerId) + if err != nil { + return nil, "ERROR", err + } + + status := utils.PathSearch("status", respBody, "").(string) + if utils.StrSliceContains([]string{"START_FAILED", "ERROR", "STOP_FAILED"}, status) { + return respBody, "ERROR", fmt.Errorf("unexpected status (%s)", status) + } + + if status == target { + return respBody, "COMPLETED", nil + } + return "continue", "PENDING", nil + } +} + +func resourceDevServerActionRead(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil +} + +func resourceDevServerActionDelete(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + errorMsg := `This resource is only a one-time action resource for operating the DevServer. Deleting this resource will +not clear the corresponding request record, but will only remove the resource information from the tfstate file.` + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: errorMsg, + }, + } +}