feat: Allow managing server state in stackit_server resource (#623)
* feat: implement state switching in resource * chore: fix linter issues * feat: fix testcases * chore: update documentation * feat: replace backoff implementation with canonical wait functionality * feat: refactor update method to correctly handle state changes of shelved servers * chore: reverted documentation changes * feat: updated server documentation * feat: configured desired_state as "write-only" attribute * feat: update to command help
This commit is contained in:
parent
e2635b5a64
commit
f04ced9981
3 changed files with 517 additions and 28 deletions
|
|
@ -384,6 +384,7 @@ resource "stackit_server" "user-data-from-file" {
|
|||
- `affinity_group` (String) The affinity group the server is assigned to.
|
||||
- `availability_zone` (String) The availability zone of the server.
|
||||
- `boot_volume` (Attributes) The boot volume for the server (see [below for nested schema](#nestedatt--boot_volume))
|
||||
- `desired_status` (String) The desired status of the server resource. Defaults to 'active' Supported values are: `active`, `inactive`, `deallocated`.
|
||||
- `image_id` (String) The image ID to be used for an ephemeral disk on the server.
|
||||
- `keypair_name` (String) The name of the keypair used during server creation.
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
|
|
|
|||
|
|
@ -47,6 +47,13 @@ var (
|
|||
_ resource.ResourceWithImportState = &serverResource{}
|
||||
|
||||
supportedSourceTypes = []string{"volume", "image"}
|
||||
desiredStatusOptions = []string{modelStateActive, modelStateInactive, modelStateDeallocated}
|
||||
)
|
||||
|
||||
const (
|
||||
modelStateActive = "active"
|
||||
modelStateInactive = "inactive"
|
||||
modelStateDeallocated = "deallocated"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
|
|
@ -65,6 +72,7 @@ type Model struct {
|
|||
CreatedAt types.String `tfsdk:"created_at"`
|
||||
LaunchedAt types.String `tfsdk:"launched_at"`
|
||||
UpdatedAt types.String `tfsdk:"updated_at"`
|
||||
DesiredStatus types.String `tfsdk:"desired_status"`
|
||||
}
|
||||
|
||||
// Struct corresponding to Model.BootVolume
|
||||
|
|
@ -333,10 +341,58 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res
|
|||
Description: "Date-time when the server was updated",
|
||||
Computed: true,
|
||||
},
|
||||
"desired_status": schema.StringAttribute{
|
||||
Description: "The desired status of the server resource." + utils.SupportedValuesDocumentation(desiredStatusOptions),
|
||||
Optional: true,
|
||||
Validators: []validator.String{
|
||||
stringvalidator.OneOf(desiredStatusOptions...),
|
||||
},
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
desiredStateModifier{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var _ planmodifier.String = desiredStateModifier{}
|
||||
|
||||
type desiredStateModifier struct {
|
||||
}
|
||||
|
||||
// Description implements planmodifier.String.
|
||||
func (d desiredStateModifier) Description(context.Context) string {
|
||||
return "validates desired state transition"
|
||||
}
|
||||
|
||||
// MarkdownDescription implements planmodifier.String.
|
||||
func (d desiredStateModifier) MarkdownDescription(ctx context.Context) string {
|
||||
return d.Description(ctx)
|
||||
}
|
||||
|
||||
// PlanModifyString implements planmodifier.String.
|
||||
func (d desiredStateModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { //nolint: gocritic //signature is defined by terraform api
|
||||
// Retrieve values from plan
|
||||
var (
|
||||
planState types.String
|
||||
currentState types.String
|
||||
)
|
||||
resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("desired_status"), &planState)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("desired_status"), ¤tState)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if currentState.ValueString() == modelStateDeallocated && planState.ValueString() == modelStateInactive {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error changing server state", "Server state change from deallocated to inactive is not possible")
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates the resource and sets the initial Terraform state.
|
||||
func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
// Retrieve values from plan
|
||||
|
|
@ -371,7 +427,6 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
|
|||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("server creation waiting: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
|
||||
// Map response body to schema
|
||||
|
|
@ -380,6 +435,12 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
|
|||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("Processing API payload: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateServerStatus(ctx, r.client, server.Status, &model); err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creting server", fmt.Sprintf("update server state: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Set state to fully populated data
|
||||
diags = resp.State.Set(ctx, model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
|
|
@ -389,6 +450,117 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
|
|||
tflog.Info(ctx, "Server created")
|
||||
}
|
||||
|
||||
// serverControlClient provides a mockable interface for the necessary
|
||||
// client operations in [updateServerStatus]
|
||||
type serverControlClient interface {
|
||||
wait.APIClientInterface
|
||||
StartServerExecute(ctx context.Context, projectId string, serverId string) error
|
||||
StopServerExecute(ctx context.Context, projectId string, serverId string) error
|
||||
DeallocateServerExecute(ctx context.Context, projectId string, serverId string) error
|
||||
}
|
||||
|
||||
func startServer(ctx context.Context, client serverControlClient, projectId, serverId string) error {
|
||||
tflog.Debug(ctx, "starting server to enter active state")
|
||||
if err := client.StartServerExecute(ctx, projectId, serverId); err != nil {
|
||||
return fmt.Errorf("cannot start server: %w", err)
|
||||
}
|
||||
_, err := wait.StartServerWaitHandler(ctx, client, projectId, serverId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot check started server: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopServer(ctx context.Context, client serverControlClient, projectId, serverId string) error {
|
||||
tflog.Debug(ctx, "stopping server to enter inactive state")
|
||||
if err := client.StopServerExecute(ctx, projectId, serverId); err != nil {
|
||||
return fmt.Errorf("cannot stop server: %w", err)
|
||||
}
|
||||
_, err := wait.StopServerWaitHandler(ctx, client, projectId, serverId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot check stopped server: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deallocatServer(ctx context.Context, client serverControlClient, projectId, serverId string) error {
|
||||
tflog.Debug(ctx, "deallocating server to enter shelved state")
|
||||
if err := client.DeallocateServerExecute(ctx, projectId, serverId); err != nil {
|
||||
return fmt.Errorf("cannot deallocate server: %w", err)
|
||||
}
|
||||
_, err := wait.DeallocateServerWaitHandler(ctx, client, projectId, serverId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot check deallocated server: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateServerStatus applies the appropriate server state changes for the actual current and the intended state
|
||||
func updateServerStatus(ctx context.Context, client serverControlClient, currentState *string, model *Model) error {
|
||||
if currentState == nil {
|
||||
tflog.Warn(ctx, "no current state available, not updating server state")
|
||||
return nil
|
||||
}
|
||||
switch *currentState {
|
||||
case wait.ServerActiveStatus:
|
||||
switch strings.ToUpper(model.DesiredStatus.ValueString()) {
|
||||
case wait.ServerInactiveStatus:
|
||||
if err := stopServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case wait.ServerDeallocatedStatus:
|
||||
|
||||
if err := deallocatServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString()))
|
||||
if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case wait.ServerInactiveStatus:
|
||||
switch strings.ToUpper(model.DesiredStatus.ValueString()) {
|
||||
case wait.ServerActiveStatus:
|
||||
if err := startServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
|
||||
return err
|
||||
}
|
||||
case wait.ServerDeallocatedStatus:
|
||||
if err := deallocatServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString()))
|
||||
if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case wait.ServerDeallocatedStatus:
|
||||
switch strings.ToUpper(model.DesiredStatus.ValueString()) {
|
||||
case wait.ServerActiveStatus:
|
||||
if err := startServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case wait.ServerInactiveStatus:
|
||||
if err := stopServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString()))
|
||||
if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
tflog.Debug(ctx, "not updating server state")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// // Read refreshes the Terraform state with the latest data.
|
||||
func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
var model Model
|
||||
|
|
@ -428,6 +600,43 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res
|
|||
tflog.Info(ctx, "server read")
|
||||
}
|
||||
|
||||
func (r *serverResource) updateServerAttributes(ctx context.Context, model, stateModel *Model) (*iaas.Server, error) {
|
||||
// Generate API request body from model
|
||||
payload, err := toUpdatePayload(ctx, model, stateModel.Labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating API payload: %w", err)
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
serverId := model.ServerId.ValueString()
|
||||
|
||||
var updatedServer *iaas.Server
|
||||
// Update existing server
|
||||
updatedServer, err = r.client.UpdateServer(ctx, projectId, serverId).UpdateServerPayload(*payload).Execute()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Calling API: %w", err)
|
||||
}
|
||||
|
||||
// Update machine type
|
||||
modelMachineType := conversion.StringValueToPointer(model.MachineType)
|
||||
if modelMachineType != nil && updatedServer.MachineType != nil && *modelMachineType != *updatedServer.MachineType {
|
||||
payload := iaas.ResizeServerPayload{
|
||||
MachineType: modelMachineType,
|
||||
}
|
||||
err := r.client.ResizeServer(ctx, projectId, serverId).ResizeServerPayload(payload).Execute()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Resizing the server, calling API: %w", err)
|
||||
}
|
||||
|
||||
_, err = wait.ResizeServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("server resize waiting: %w", err)
|
||||
}
|
||||
// Update server model because the API doesn't return a server object as response
|
||||
updatedServer.MachineType = modelMachineType
|
||||
}
|
||||
return updatedServer, nil
|
||||
}
|
||||
|
||||
// Update updates the resource and sets the updated Terraform state on success.
|
||||
func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
// Retrieve values from plan
|
||||
|
|
@ -450,37 +659,40 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
|
|||
return
|
||||
}
|
||||
|
||||
// Generate API request body from model
|
||||
payload, err := toUpdatePayload(ctx, &model, stateModel.Labels)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
}
|
||||
// Update existing server
|
||||
updatedServer, err := r.client.UpdateServer(ctx, projectId, serverId).UpdateServerPayload(*payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
var (
|
||||
server *iaas.Server
|
||||
err error
|
||||
)
|
||||
if server, err = r.client.GetServer(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()).Execute(); err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error retrieving server state", fmt.Sprintf("Getting server state: %v", err))
|
||||
}
|
||||
|
||||
// Update machine type
|
||||
modelMachineType := conversion.StringValueToPointer(model.MachineType)
|
||||
if modelMachineType != nil && updatedServer.MachineType != nil && *modelMachineType != *updatedServer.MachineType {
|
||||
payload := iaas.ResizeServerPayload{
|
||||
MachineType: modelMachineType,
|
||||
}
|
||||
err := r.client.ResizeServer(ctx, projectId, serverId).ResizeServerPayload(payload).Execute()
|
||||
var updatedServer *iaas.Server
|
||||
if model.DesiredStatus.ValueString() == modelStateDeallocated {
|
||||
// if the target state is "deallocated", we have to perform the server update first
|
||||
// and then shelve it afterwards. A shelved server cannot be updated
|
||||
updatedServer, err = r.updateServerAttributes(ctx, &model, &stateModel)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("Resizing the server, calling API: %v", err))
|
||||
}
|
||||
|
||||
_, err = wait.ResizeServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("server resize waiting: %v", err))
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateServerStatus(ctx, r.client, server.Status, &model); err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// potentially unfreeze first and update afterwards
|
||||
if err := updateServerStatus(ctx, r.client, server.Status, &model); err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updatedServer, err = r.updateServerAttributes(ctx, &model, &stateModel)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error())
|
||||
return
|
||||
}
|
||||
// Update server model because the API doesn't return a server object as response
|
||||
updatedServer.MachineType = modelMachineType
|
||||
}
|
||||
|
||||
err = mapFields(ctx, updatedServer, &model)
|
||||
|
|
@ -488,6 +700,7 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
|
|||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("Processing API payload: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
diags = resp.State.Set(ctx, model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
|
|
@ -605,7 +818,15 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error
|
|||
|
||||
model.ServerId = types.StringValue(serverId)
|
||||
model.MachineType = types.StringPointerValue(serverResp.MachineType)
|
||||
model.AvailabilityZone = types.StringPointerValue(serverResp.AvailabilityZone)
|
||||
|
||||
// Proposed fix: If the server is deallocated, it has no availability zone anymore
|
||||
// reactivation will then _change_ the availability zone again, causing terraform
|
||||
// to destroy and recreate the resource, which is not intended. So we skip the zone
|
||||
// when the server is deallocated to retain the original zone until the server
|
||||
// is activated again
|
||||
if serverResp.Status != nil && *serverResp.Status != wait.ServerDeallocatedStatus {
|
||||
model.AvailabilityZone = types.StringPointerValue(serverResp.AvailabilityZone)
|
||||
}
|
||||
model.Name = types.StringPointerValue(serverResp.Name)
|
||||
model.Labels = labels
|
||||
model.ImageId = types.StringPointerValue(serverResp.ImageId)
|
||||
|
|
@ -614,6 +835,7 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error
|
|||
model.CreatedAt = createdAt
|
||||
model.UpdatedAt = updatedAt
|
||||
model.LaunchedAt = launchedAt
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import (
|
|||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/utils"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -76,6 +78,7 @@ func TestMapFields(t *testing.T) {
|
|||
CreatedAt: utils.Ptr(testTimestamp()),
|
||||
UpdatedAt: utils.Ptr(testTimestamp()),
|
||||
LaunchedAt: utils.Ptr(testTimestamp()),
|
||||
Status: utils.Ptr("active"),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,sid"),
|
||||
|
|
@ -267,3 +270,266 @@ func TestToUpdatePayload(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
var _ serverControlClient = (*mockServerControlClient)(nil)
|
||||
|
||||
// mockServerControlClient mocks the [serverControlClient] interface with
|
||||
// pluggable functions
|
||||
type mockServerControlClient struct {
|
||||
wait.APIClientInterface
|
||||
startServerCalled int
|
||||
startServerExecute func(callNo int, ctx context.Context, projectId, serverId string) error
|
||||
|
||||
stopServerCalled int
|
||||
stopServerExecute func(callNo int, ctx context.Context, projectId, serverId string) error
|
||||
|
||||
deallocateServerCalled int
|
||||
deallocateServerExecute func(callNo int, ctx context.Context, projectId, serverId string) error
|
||||
|
||||
getServerCalled int
|
||||
getServerExecute func(callNo int, ctx context.Context, projectId, serverId string) (*iaas.Server, error)
|
||||
}
|
||||
|
||||
// DeallocateServerExecute implements serverControlClient.
|
||||
func (t *mockServerControlClient) DeallocateServerExecute(ctx context.Context, projectId, serverId string) error {
|
||||
t.deallocateServerCalled++
|
||||
return t.deallocateServerExecute(t.deallocateServerCalled, ctx, projectId, serverId)
|
||||
}
|
||||
|
||||
// GetServerExecute implements serverControlClient.
|
||||
func (t *mockServerControlClient) GetServerExecute(ctx context.Context, projectId, serverId string) (*iaas.Server, error) {
|
||||
t.getServerCalled++
|
||||
return t.getServerExecute(t.getServerCalled, ctx, projectId, serverId)
|
||||
}
|
||||
|
||||
// StartServerExecute implements serverControlClient.
|
||||
func (t *mockServerControlClient) StartServerExecute(ctx context.Context, projectId, serverId string) error {
|
||||
t.startServerCalled++
|
||||
return t.startServerExecute(t.startServerCalled, ctx, projectId, serverId)
|
||||
}
|
||||
|
||||
// StopServerExecute implements serverControlClient.
|
||||
func (t *mockServerControlClient) StopServerExecute(ctx context.Context, projectId, serverId string) error {
|
||||
t.stopServerCalled++
|
||||
return t.stopServerExecute(t.stopServerCalled, ctx, projectId, serverId)
|
||||
}
|
||||
|
||||
func Test_serverResource_updateServerStatus(t *testing.T) {
|
||||
projectId := basetypes.NewStringValue("projectId")
|
||||
serverId := basetypes.NewStringValue("serverId")
|
||||
type fields struct {
|
||||
client *mockServerControlClient
|
||||
}
|
||||
type args struct {
|
||||
currentState *string
|
||||
model Model
|
||||
}
|
||||
type want struct {
|
||||
err bool
|
||||
status types.String
|
||||
getServerCount int
|
||||
stopCount int
|
||||
startCount int
|
||||
deallocatedCount int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "no desired status",
|
||||
fields: fields{
|
||||
client: &mockServerControlClient{
|
||||
getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) {
|
||||
return &iaas.Server{
|
||||
Id: utils.Ptr(serverId.ValueString()),
|
||||
Status: utils.Ptr(wait.ServerActiveStatus),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
currentState: utils.Ptr(wait.ServerActiveStatus),
|
||||
model: Model{
|
||||
ProjectId: projectId,
|
||||
ServerId: serverId,
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
getServerCount: 1,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "desired inactive state",
|
||||
fields: fields{
|
||||
client: &mockServerControlClient{
|
||||
getServerExecute: func(no int, _ context.Context, _, _ string) (*iaas.Server, error) {
|
||||
var state string
|
||||
if no <= 1 {
|
||||
state = wait.ServerActiveStatus
|
||||
} else {
|
||||
state = wait.ServerInactiveStatus
|
||||
}
|
||||
return &iaas.Server{
|
||||
Id: utils.Ptr(serverId.ValueString()),
|
||||
Status: &state,
|
||||
}, nil
|
||||
},
|
||||
stopServerExecute: func(_ int, _ context.Context, _, _ string) error { return nil },
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
currentState: utils.Ptr(wait.ServerActiveStatus),
|
||||
model: Model{
|
||||
ProjectId: projectId,
|
||||
ServerId: serverId,
|
||||
DesiredStatus: basetypes.NewStringValue("inactive"),
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
getServerCount: 2,
|
||||
stopCount: 1,
|
||||
status: basetypes.NewStringValue("inactive"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "desired deallocated state",
|
||||
fields: fields{
|
||||
client: &mockServerControlClient{
|
||||
getServerExecute: func(no int, _ context.Context, _, _ string) (*iaas.Server, error) {
|
||||
var state string
|
||||
switch no {
|
||||
case 1:
|
||||
state = wait.ServerActiveStatus
|
||||
case 2:
|
||||
state = wait.ServerInactiveStatus
|
||||
default:
|
||||
state = wait.ServerDeallocatedStatus
|
||||
}
|
||||
return &iaas.Server{
|
||||
Id: utils.Ptr(serverId.ValueString()),
|
||||
Status: &state,
|
||||
}, nil
|
||||
},
|
||||
deallocateServerExecute: func(_ int, _ context.Context, _, _ string) error { return nil },
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
currentState: utils.Ptr(wait.ServerActiveStatus),
|
||||
model: Model{
|
||||
ProjectId: projectId,
|
||||
ServerId: serverId,
|
||||
DesiredStatus: basetypes.NewStringValue("deallocated"),
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
getServerCount: 3,
|
||||
deallocatedCount: 1,
|
||||
status: basetypes.NewStringValue("deallocated"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "don't call start if active",
|
||||
fields: fields{
|
||||
client: &mockServerControlClient{
|
||||
getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) {
|
||||
return &iaas.Server{
|
||||
Id: utils.Ptr(serverId.ValueString()),
|
||||
Status: utils.Ptr(wait.ServerActiveStatus),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
currentState: utils.Ptr(wait.ServerActiveStatus),
|
||||
model: Model{
|
||||
ProjectId: projectId,
|
||||
ServerId: serverId,
|
||||
DesiredStatus: basetypes.NewStringValue("active"),
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
status: basetypes.NewStringValue("active"),
|
||||
getServerCount: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "don't call stop if inactive",
|
||||
fields: fields{
|
||||
client: &mockServerControlClient{
|
||||
getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) {
|
||||
return &iaas.Server{
|
||||
Id: utils.Ptr(serverId.ValueString()),
|
||||
Status: utils.Ptr(wait.ServerInactiveStatus),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
currentState: utils.Ptr(wait.ServerInactiveStatus),
|
||||
model: Model{
|
||||
ProjectId: projectId,
|
||||
ServerId: serverId,
|
||||
DesiredStatus: basetypes.NewStringValue("inactive"),
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
status: basetypes.NewStringValue("inactive"),
|
||||
getServerCount: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "don't call dealloacate if deallocated",
|
||||
fields: fields{
|
||||
client: &mockServerControlClient{
|
||||
getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) {
|
||||
return &iaas.Server{
|
||||
Id: utils.Ptr(serverId.ValueString()),
|
||||
Status: utils.Ptr(wait.ServerDeallocatedStatus),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
currentState: utils.Ptr(wait.ServerDeallocatedStatus),
|
||||
model: Model{
|
||||
ProjectId: projectId,
|
||||
ServerId: serverId,
|
||||
DesiredStatus: basetypes.NewStringValue("deallocated"),
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
status: basetypes.NewStringValue("deallocated"),
|
||||
getServerCount: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := updateServerStatus(context.Background(), tt.fields.client, tt.args.currentState, &tt.args.model)
|
||||
if (err != nil) != tt.want.err {
|
||||
t.Errorf("inconsistent error, want %v and got %v", tt.want.err, err)
|
||||
}
|
||||
if expected, actual := tt.want.status, tt.args.model.DesiredStatus; expected != actual {
|
||||
t.Errorf("wanted status %s but got %s", expected, actual)
|
||||
}
|
||||
|
||||
if expected, actual := tt.want.getServerCount, tt.fields.client.getServerCalled; expected != actual {
|
||||
t.Errorf("wrong number of get server calls: Expected %d but got %d", expected, actual)
|
||||
}
|
||||
if expected, actual := tt.want.startCount, tt.fields.client.startServerCalled; expected != actual {
|
||||
t.Errorf("wrong number of start server calls: Expected %d but got %d", expected, actual)
|
||||
}
|
||||
if expected, actual := tt.want.stopCount, tt.fields.client.stopServerCalled; expected != actual {
|
||||
t.Errorf("wrong number of stop server calls: Expected %d but got %d", expected, actual)
|
||||
}
|
||||
if expected, actual := tt.want.deallocatedCount, tt.fields.client.deallocateServerCalled; expected != actual {
|
||||
t.Errorf("wrong number of deallocate server calls: Expected %d but got %d", expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue