Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(modelarts): add resource to operate devserver #6098

Merged
merged 1 commit into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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.
1 change: 1 addition & 0 deletions huaweicloud/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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,
Expand All @@ -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

Expand All @@ -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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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" {
// For the "start" type, the request body parameter must be specified, so set an empty object for it.
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,
},
}
}
Loading