Skip to content

Commit

Permalink
feat(modelarts): add resource to operate devserver
Browse files Browse the repository at this point in the history
  • Loading branch information
wuzhuanhong committed Dec 26, 2024
1 parent 59b4f99 commit 908a457
Show file tree
Hide file tree
Showing 4 changed files with 360 additions and 0 deletions.
44 changes: 44 additions & 0 deletions docs/resources/modelarts_devserver_action.md
Original file line number Diff line number Diff line change
@@ -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 DevServers is stopped, fails to stop, or fails to start.
+ **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.
1 change: 1 addition & 0 deletions huaweicloud/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1909,6 +1909,7 @@ func Provider() *schema.Provider {

"huaweicloud_modelarts_dataset": modelarts.ResourceDataset(),
"huaweicloud_modelarts_dataset_version": modelarts.ResourceDatasetVersion(),
"huaweicloud_modelarts_devserver_action": modelarts.ResourceDevServerAction(),
"huaweicloud_modelarts_notebook": modelarts.ResourceNotebook(),
"huaweicloud_modelarts_notebook_mount_storage": modelarts.ResourceNotebookMountStorage(),
"huaweicloud_modelarts_model": modelarts.ResourceModelartsModel(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package modelarts

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"

"github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config"
"github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/acceptance"
"github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/acceptance/common"
"github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/modelarts"
)

func getDevServerResourceFunc(cfg *config.Config, state *terraform.ResourceState) (interface{}, error) {
region := acceptance.HW_REGION_NAME
client, err := cfg.NewServiceClient("modelarts", region)
if err != nil {
return nil, fmt.Errorf("error creating ModelArts client: %s", err)
}

return modelarts.GetDevServerById(client, state.Primary.ID)
}

func TestAccDevServer_basic(t *testing.T) {
var (
obj interface{}
resourceName = "huaweicloud_modelarts_devserver.test"
name = acceptance.RandomAccResourceName()
password = acceptance.RandomPassword("!@%-_=+[{}]:,./?")
rc = acceptance.InitResourceCheck(
resourceName,
&obj,
getDevServerResourceFunc,
)
)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
acceptance.TestAccPreCheck(t)
acceptance.TestAccPreCheckModelartsDevServer(t)
},
ProviderFactories: acceptance.TestAccProviderFactories,
CheckDestroy: rc.CheckResourceDestroy(),
Steps: []resource.TestStep{
{
Config: testDevServer_basic_step1(name, password, true),
Check: resource.ComposeTestCheckFunc(
rc.CheckResourceExists(),
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttr(resourceName, "flavor", acceptance.HW_MODELARTS_DEVSERVER_FLAVOR),
resource.TestCheckResourceAttrSet(resourceName, "architecture"),
resource.TestCheckResourceAttrPair(resourceName, "vpc_id", "huaweicloud_vpc.test", "id"),
resource.TestCheckResourceAttrPair(resourceName, "subnet_id", "huaweicloud_vpc_subnet.test", "id"),
resource.TestCheckResourceAttrPair(resourceName, "security_group_id", "huaweicloud_networking_secgroup.test", "id"),
resource.TestCheckResourceAttr(resourceName, "image_id", acceptance.HW_MODELARTS_DEVSERVER_IMAGE_ID),
resource.TestCheckResourceAttrSet(resourceName, "type"),
resource.TestCheckResourceAttr(resourceName, "charging_mode", "PRE_PAID"),
resource.TestCheckResourceAttr(resourceName, "auto_renew", "true"),
resource.TestMatchResourceAttr(resourceName, "created_at",
regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}?(Z|([+-]\d{2}:\d{2}))$`)),
),
},
{
Config: testDevServer_basic_step2(name, password, false),
Check: resource.ComposeTestCheckFunc(
rc.CheckResourceExists(),
resource.TestCheckResourceAttr(resourceName, "auto_renew", "false"),
),
},
// Stopping the DevServer.
{
Config: testDevServer_basic_step3(name, password, false),
},
// Stopping the stopped DevServer.
{
Config: testDevServer_basic_step4(name, password, false),
ExpectError: regexp.MustCompile(`Resource.Server '[a-f0-9-]+' is not allowed STOP: STOPPED`),
},
// Starting the DevServer.
{
Config: testDevServer_basic_step5(name, password, false),
},
// Starting the running DevServer.
{
Config: testDevServer_basic_step6(name, password, false),
ExpectError: regexp.MustCompile(`Resource.Server '[a-f0-9-]+' is not allowed START: RUNNING`),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"subnet_id",
"security_group_id",
"admin_pass",
"root_volume",
"period_unit",
"period",
"auto_renew",
},
},
},
})
}

func testDevServer_basic_step1(name, password string, autoRenew bool) string {
return fmt.Sprintf(`
%[1]s
resource "huaweicloud_modelarts_devserver" "test" {
name = "%[2]s"
flavor = "%[3]s"
vpc_id = huaweicloud_vpc.test.id
subnet_id = huaweicloud_vpc_subnet.test.id
security_group_id = huaweicloud_networking_secgroup.test.id
image_id = "%[4]s"
admin_pass = "%[5]s"
root_volume {
size = 100
type = "SSD"
}
charging_mode = "PRE_PAID"
period_unit = "MONTH"
period = 1
auto_renew = "%[6]v"
}
`, common.TestBaseNetwork(name), name,
acceptance.HW_MODELARTS_DEVSERVER_FLAVOR, acceptance.HW_MODELARTS_DEVSERVER_IMAGE_ID, password, autoRenew)
}

func testDevServer_basic_step2(name, password string, autoRenew bool) string {
return testDevServer_basic_step1(name, password, autoRenew)
}

func testDevServer_basic_step3(name, password string, autoRenew bool) string {
return fmt.Sprintf(`
%[1]s
resource "huaweicloud_modelarts_devserver_action" "stop" {
devserver_id = huaweicloud_modelarts_devserver.test.id
action = "stop"
}
`, testDevServer_basic_step2(name, password, autoRenew))
}

func testDevServer_basic_step4(name, password string, autoRenew bool) string {
return fmt.Sprintf(`
%[1]s
resource "huaweicloud_modelarts_devserver_action" "expect_stop_err" {
devserver_id = huaweicloud_modelarts_devserver.test.id
action = "stop"
}
`, testDevServer_basic_step3(name, password, autoRenew))
}

func testDevServer_basic_step5(name, password string, autoRenew bool) string {
return fmt.Sprintf(`
%[1]s
resource "huaweicloud_modelarts_devserver_action" "start" {
devserver_id = huaweicloud_modelarts_devserver.test.id
action = "start"
}
`, testDevServer_basic_step3(name, password, autoRenew))
}

func testDevServer_basic_step6(name, password string, autoRenew bool) string {
return fmt.Sprintf(`
%[1]s
resource "huaweicloud_modelarts_devserver_action" "expect_start_err" {
devserver_id = huaweicloud_modelarts_devserver.test.id
action = "start"
}
`, testDevServer_basic_step5(name, password, autoRenew))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package modelarts

import (
"context"
"fmt"
"strings"
"time"

"github.com/chnsz/golangsdk"
"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/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)

Check failure on line 103 in huaweicloud/services/modelarts/resource_huaweicloud_modelarts_devserver_action.go

View workflow job for this annotation

GitHub Actions / build

undefined: GetDevServerById

Check failure on line 103 in huaweicloud/services/modelarts/resource_huaweicloud_modelarts_devserver_action.go

View workflow job for this annotation

GitHub Actions / golangci

undefined: GetDevServerById (typecheck)

Check failure on line 103 in huaweicloud/services/modelarts/resource_huaweicloud_modelarts_devserver_action.go

View workflow job for this annotation

GitHub Actions / golangci

undefined: GetDevServerById) (typecheck)
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,
},
}
}

0 comments on commit 908a457

Please sign in to comment.