Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/acceptance-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,4 @@ jobs:
SCW_SECRET_KEY: "11111111-1111-1111-1111-111111111111"
SCW_ENABLE_BETA: true
TF_ACC_LOG: trace
TF_ACC_OPENTOFU: true
3 changes: 2 additions & 1 deletion .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version: stable
- uses: hashicorp/setup-terraform@v3
- name: Install Terraform
uses: hashicorp/setup-terraform@v3
- run: go tool tfplugindocs validate
- run: rm -fr ./docs
- run: go tool tfplugindocs generate
Expand Down
21 changes: 21 additions & 0 deletions docs/actions/instance_server_action.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
subcategory: "Instances"
page_title: "Scaleway: scaleway_instance_server_action"
---

# scaleway_instance_server_action (Action)

<!-- action schema generated by tfplugindocs -->
## Schema

### Required

- `action` (String) Type of action to perform
- `server_id` (String) Server id to send the action to

### Optional

- `wait` (Boolean) Wait for server to finish action
- `zone` (String) Zone of server to send the action to


1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/hashicorp/terraform-plugin-framework v1.16.0
github.com/hashicorp/terraform-plugin-framework-validators v0.18.1-0.20250909114857-8e55d8ccabdb
github.com/hashicorp/terraform-plugin-go v0.29.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-mux v0.21.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@ github.com/hashicorp/terraform-plugin-docs v0.24.0 h1:YNZYd+8cpYclQyXbl1EEngbld8
github.com/hashicorp/terraform-plugin-docs v0.24.0/go.mod h1:YLg+7LEwVmRuJc0EuCw0SPLxuQXw5mW8iJ5ml/kvi+o=
github.com/hashicorp/terraform-plugin-framework v1.16.0 h1:tP0f+yJg0Z672e7levixDe5EpWwrTrNryPM9kDMYIpE=
github.com/hashicorp/terraform-plugin-framework v1.16.0/go.mod h1:0xFOxLy5lRzDTayc4dzK/FakIgBhNf/lC4499R9cV4Y=
github.com/hashicorp/terraform-plugin-framework-validators v0.18.1-0.20250909114857-8e55d8ccabdb h1:wRiOv+xaGRrBuc8r774OtrELwQCiSLLQNrkH00ZLO90=
github.com/hashicorp/terraform-plugin-framework-validators v0.18.1-0.20250909114857-8e55d8ccabdb/go.mod h1:vU2y54LtDNHGLjDD7LH/if+4KBKZ5ljTrgDdrM6y8Pc=
github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU=
github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
Expand Down
11 changes: 11 additions & 0 deletions internal/acctest/opentofu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package acctest

import (
"os"

"github.com/scaleway/terraform-provider-scaleway/v2/internal/env"
)

func IsRunningOpenTofu() bool {
return os.Getenv(env.AccRunningOpenTofu) == "true"
}
3 changes: 3 additions & 0 deletions internal/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ const (
AppendUserAgent = "TF_APPEND_USER_AGENT"
// AccDomainRegistration if set to "true" will trigger acceptance test for domain registration
AccDomainRegistration = "TF_ACC_DOMAIN_REGISTRATION"
// AccRunningOpenTofu is set to "true" in the CI to document that we are using OpenTofu. It can be helpful to skip
// tests that are not yet compatible with OpenTofu
AccRunningOpenTofu = "TF_ACC_OPENTOFU"
)
145 changes: 145 additions & 0 deletions internal/services/instance/action_server_action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package instance

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/action"
"github.com/hashicorp/terraform-plugin-framework/action/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/meta"
)

var (
_ action.Action = (*ServerAction)(nil)
_ action.ActionWithConfigure = (*ServerAction)(nil)
)

type ServerAction struct {
instanceAPI *instance.API
}

func (a *ServerAction) Configure(ctx context.Context, req action.ConfigureRequest, resp *action.ConfigureResponse) {
if req.ProviderData == nil {
return
}

m, ok := req.ProviderData.(*meta.Meta)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Action Configure Type",
fmt.Sprintf("Expected *scw.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

client := m.ScwClient()
a.instanceAPI = instance.NewAPI(client)
}

func (a *ServerAction) Metadata(ctx context.Context, req action.MetadataRequest, resp *action.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_instance_server_action"
}

type ServerActionModel struct {
ServerID types.String `tfsdk:"server_id"`
Zone types.String `tfsdk:"zone"`
Action types.String `tfsdk:"action"`
Wait types.Bool `tfsdk:"wait"`
}

func NewServerAction() action.Action {
return &ServerAction{}
}

func (a *ServerAction) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) {
actionsValues := instance.ServerAction("").Values()

actionStringValues := make([]string, 0, len(actionsValues))
for _, actionValue := range actionsValues {
actionStringValues = append(actionStringValues, actionValue.String())
}

resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"action": schema.StringAttribute{
Required: true,
Description: "Type of action to perform",
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive(actionStringValues...),
},
},
"server_id": schema.StringAttribute{
Required: true,
Description: "Server id to send the action to",
},
"zone": schema.StringAttribute{
Optional: true,
Description: "Zone of server to send the action to",
},
"wait": schema.BoolAttribute{
Optional: true,
Description: "Wait for server to finish action",
},
},
}
}

func (a *ServerAction) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
var data ServerActionModel
// Read action config data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

if a.instanceAPI == nil {
resp.Diagnostics.AddError(
"Unconfigured instanceAPI",
"The action was not properly configured. The Scaleway client is missing. "+
"This is usually a bug in the provider. Please report it to the maintainers.",
)

return
}

actionReq := &instance.ServerActionRequest{
ServerID: locality.ExpandID(data.ServerID.ValueString()),
Action: instance.ServerAction(data.Action.ValueString()),
}
if !data.Zone.IsNull() {
actionReq.Zone = scw.Zone(data.Zone.String())
}

_, err := a.instanceAPI.ServerAction(actionReq)
if err != nil {
resp.Diagnostics.AddError(
"error in server action",
fmt.Sprintf("%s", err))
}

if data.Wait.ValueBool() {
waitReq := &instance.WaitForServerRequest{
ServerID: locality.ExpandID(data.ServerID.ValueString()),
Zone: scw.Zone(data.Zone.String()),
}

if !data.Zone.IsNull() {
waitReq.Zone = scw.Zone(data.Zone.String())
}

_, errWait := a.instanceAPI.WaitForServer(waitReq)
if errWait != nil {
resp.Diagnostics.AddError(
"error in wait server",
fmt.Sprintf("%s", err))
}
}
}
126 changes: 126 additions & 0 deletions internal/services/instance/action_server_action_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package instance_test

import (
"errors"
"fmt"
"regexp"
"strconv"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest"
)

func TestAccActionServer_Basic(t *testing.T) {
if acctest.IsRunningOpenTofu() {
t.Skip("Skipping TestAccActionServer_Basic because action are not yet supported on OpenTofu")
}

tt := acctest.NewTestTools(t)
defer tt.Cleanup()

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: tt.ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
resource "scaleway_instance_server" "main" {
name = "test-terraform-datasource-private-nic"
type = "DEV1-S"
image = "ubuntu_jammy"

lifecycle {
action_trigger {
events = [after_create]
actions = [action.scaleway_instance_server_action.main]
}
}
}

action "scaleway_instance_server_action" "main" {
config {
action = "reboot"
server_id = scaleway_instance_server.main.id
}
}`,
},
{
Config: `
resource "scaleway_instance_server" "main" {
name = "test-terraform-datasource-private-nic"
type = "DEV1-S"
image = "ubuntu_jammy"

lifecycle {
action_trigger {
events = [after_create]
actions = [action.scaleway_instance_server_action.main]
}
}
}

action "scaleway_instance_server_action" "main" {
config {
action = "reboot"
server_id = scaleway_instance_server.main.id
}
}

data "scaleway_audit_trail_event" "instance" {
resource_type = "instance_server"
resource_id = scaleway_instance_server.main.id
method_name = "ServerAction"
}`,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("data.scaleway_audit_trail_event.instance", "events.#"),
func(state *terraform.State) error {
rs, ok := state.RootModule().Resources["data.scaleway_audit_trail_event.instance"]
if !ok {
return errors.New("not found: data.scaleway_audit_trail_event.instance")
}

countStr := rs.Primary.Attributes["events.#"]

count, err := strconv.Atoi(countStr)
if err != nil {
return fmt.Errorf("could not parse events.# as integer: %w", err)
}

if count < 1 {
return fmt.Errorf("expected events count > 1, got %d", count)
}

return nil
},
),
Comment on lines +75 to +96
Copy link
Contributor

@estellesoulard estellesoulard Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Shouldn't the check be if count < 2? At this point we expect a poweron followed by a reboot (and 2 matching events).
  • Add check on the latest event's request_body matching "action": "reboot"
  • Maybe add a PreConfig wait to this test (like this one) to wait for events to cleanly pop up and secure the test results?

},
},
})
}

func TestAccActionServer_UnknownVerb(t *testing.T) {
if acctest.IsRunningOpenTofu() {
t.Skip("Skipping TestAccActionServer_Basic because action are not yet supported on OpenTofu")
}

tt := acctest.NewTestTools(t)
defer tt.Cleanup()

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: tt.ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
action "scaleway_instance_server_action" "main" {
config {
action = "unknownVerb"
server_id = "11111111-1111-1111-1111-111111111111"
}
}
`,
ExpectError: regexp.MustCompile("Invalid Attribute Value Match"),
},
},
})
}
1,661 changes: 1,661 additions & 0 deletions internal/services/instance/testdata/action-server-basic.cassette.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
version: 2
interactions: []
Loading
Loading