Skip to content

Commit

Permalink
Merge pull request #39 from k-yomo/set-transient-strategy-to-autodetect
Browse files Browse the repository at this point in the history
Fix prohibited transient config update error
  • Loading branch information
k-yomo authored Nov 6, 2023
2 parents f9cf015 + 9849039 commit 12a8ae3
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 2 deletions.
46 changes: 44 additions & 2 deletions pkg/elasticcloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,39 @@ func (c *clientImpl) GetESResourceInfo(ctx context.Context, includePlanHistory b
return resp.Payload.Resources.Elasticsearch[0], nil
}

func (c *clientImpl) getESResourceInfoForUpdate(ctx context.Context) (*models.ElasticsearchResourceInfo, error) {
params := &deployments.GetDeploymentParams{
DeploymentID: c.deploymentID,
ShowSettings: ec.Bool(true),
ShowPlans: ec.Bool(true),
ShowPlanDefaults: ec.Bool(true),
ShowMetadata: ec.Bool(true),
Context: ctx,
}
resp, err := c.ecAPI.V1API.Deployments.GetDeployment(params, c.ecAPI.AuthWriter)
if err != nil {
return nil, err
}
if len(resp.Payload.Resources.Elasticsearch) == 0 {
return nil, errors.New("elasticsearch resource is not found")
}

// sometimes transient is set by system update
// set it to autodetect here because transient configuration change is prohibited
currentPlan := resp.Payload.Resources.Elasticsearch[0].Info.PlanInfo.Current.Plan
if currentPlan.Transient != nil {
currentPlan.Transient = &models.TransientElasticsearchPlanConfiguration{
Strategy: &models.PlanStrategy{
Autodetect: struct{}{},
},
}
}

return resp.Payload.Resources.Elasticsearch[0], nil
}

func (c *clientImpl) UpdateESHotContentTopologySize(ctx context.Context, updatedTopology *models.TopologySize) error {
esResource, err := c.GetESResourceInfo(ctx, false)
esResource, err := c.getESResourceInfoForUpdate(ctx)
if err != nil {
return err
}
Expand Down Expand Up @@ -83,10 +114,21 @@ func (c *clientImpl) UpdateESHotContentTopologySize(ctx context.Context, updated
if err := WaitForPlanCompletion(c.ecAPI, c.deploymentID); err != nil {
return fmt.Errorf("wait for plan completion: %w", err)
}

updatedResourceInfo, err := c.GetESResourceInfo(ctx, false)
if err != nil {
return fmt.Errorf("get updated resource info after plan completion: %w", err)
}
if updatedResourceInfo.Info.PlanInfo.Pending != nil {
return errors.New("pending plan still exists")
}
if updatedResourceInfo.Info.PlanInfo.Current.Error != nil {
return fmt.Errorf("update topology plan failed: %s", formatPlanAttemptError(updatedResourceInfo.Info.PlanInfo.Current.Error))
}
return nil
}

const (
var (
defaultPollPlanFrequency = 2 * time.Second
defaultMaxPlanRetry = 4
)
Expand Down
255 changes: 255 additions & 0 deletions pkg/elasticcloud/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package elasticcloud

import (
"context"
"fmt"
"net/url"
"testing"
"time"

"github.com/elastic/cloud-sdk-go/pkg/api"
"github.com/elastic/cloud-sdk-go/pkg/api/mock"
"github.com/elastic/cloud-sdk-go/pkg/models"
planmock "github.com/elastic/cloud-sdk-go/pkg/plan/mock"
"github.com/elastic/cloud-sdk-go/pkg/util/ec"
)

const mockDeploymentID = "11111111111111111111111111111111"

var defaultMockDeploymentGetResponse = &models.DeploymentGetResponse{
Healthy: ec.Bool(true),
ID: ec.String(mock.ValidClusterID),
Resources: &models.DeploymentResources{
Elasticsearch: []*models.ElasticsearchResourceInfo{
{
Info: &models.ElasticsearchClusterInfo{
PlanInfo: &models.ElasticsearchClusterPlansInfo{
Current: &models.ElasticsearchClusterPlanInfo{
Plan: &models.ElasticsearchClusterPlan{
ClusterTopology: []*models.ElasticsearchClusterTopologyElement{
{
ID: string(TopologyIDHotContent),
Size: NewTopologySize(256),
},
{
ID: string(TopologyIDMaster),
Size: NewTopologySize(64),
},
},
Transient: &models.TransientElasticsearchPlanConfiguration{
PlanConfiguration: &models.ElasticsearchPlanControlConfiguration{
MoveOnly: ec.Bool(true),
},
},
},
},
},
},
},
},
},
}

func Test_clientImpl_UpdateESHotContentTopologySize(t *testing.T) {
t.Parallel()

defaultPollPlanFrequency = 1 * time.Millisecond
defaultMaxPlanRetry = 1

tests := []struct {
name string
ecAPI *api.API
deploymentID string
updatedTopology *models.TopologySize
wantErr bool
}{
{
name: "returns null when topology is updated successfully",
deploymentID: mockDeploymentID,
updatedTopology: NewTopologySize(512),
ecAPI: api.NewMock(
mock.New200Response(mock.NewStructBody(defaultMockDeploymentGetResponse)),
mock.New200ResponseAssertion(
&mock.RequestAssertion{
Host: api.DefaultMockHost,
Header: api.DefaultWriteMockHeaders,
Method: "PUT",
Path: fmt.Sprintf("/api/v1/deployments/%s", mockDeploymentID),
Query: url.Values{
"hide_pruned_orphans": []string{"false"},
"skip_snapshot": []string{"false"},
},
Body: mock.NewStructBody(
models.DeploymentUpdateRequest{
PruneOrphans: ec.Bool(false),
Resources: &models.DeploymentUpdateResources{
Elasticsearch: []*models.ElasticsearchPayload{
{
Plan: &models.ElasticsearchClusterPlan{
ClusterTopology: []*models.ElasticsearchClusterTopologyElement{
{
ID: string(TopologyIDHotContent),
Size: NewTopologySize(512),
},
{
ID: string(TopologyIDMaster),
Size: NewTopologySize(64),
},
},
// plan configuration is set to nil
Transient: &models.TransientElasticsearchPlanConfiguration{
Strategy: &models.PlanStrategy{
Autodetect: struct{}{},
},
},
},
},
},
},
},
),
},
mock.NewStringBody(`{}`), // dummy
),
mock.New200Response(mock.NewStructBody(
planmock.Generate(planmock.GenerateConfig{
ID: "cbb4bc6c09684c86aa5de54c05ea1d38",
Elasticsearch: []planmock.GeneratedResourceConfig{},
})),
),
mock.New200Response(mock.NewStructBody(
planmock.Generate(planmock.GenerateConfig{
ID: "cbb4bc6c09684c86aa5de54c05ea1d38",
Elasticsearch: []planmock.GeneratedResourceConfig{
{
ID: "cde7b6b605424a54ce9d56316eab13a1",
PendingLog: nil,
CurrentLog: planmock.NewPlanStepLog(
planmock.NewPlanStep("plan-completed", "success"),
),
},
},
})),
),
mock.New200Response(mock.NewStructBody(defaultMockDeploymentGetResponse)),
),
wantErr: false,
},
{
name: "returns error when topology update plan completed with error",
deploymentID: mockDeploymentID,
updatedTopology: NewTopologySize(512),
ecAPI: api.NewMock(
mock.New200Response(mock.NewStructBody(defaultMockDeploymentGetResponse)),
mock.New200Response(mock.NewStringBody(`{}`)), // updated
mock.New200Response(mock.NewStructBody(
planmock.Generate(planmock.GenerateConfig{
ID: mockDeploymentID,
Elasticsearch: []planmock.GeneratedResourceConfig{
{
ID: "cde7b6b605424a54ce9d56316eab13a1",
PendingLog: nil,
CurrentLog: planmock.NewPlanStepLog(
planmock.NewPlanStep("step-1", "success"),
planmock.NewPlanStep("plan-completed", "error"),
),
},
},
})),
),
mock.New200Response(mock.NewStructBody(
planmock.Generate(planmock.GenerateConfig{
ID: mockDeploymentID,
Elasticsearch: []planmock.GeneratedResourceConfig{
{
ID: "cde7b6b605424a54ce9d56316eab13a1",
PendingLog: nil,
CurrentLog: planmock.NewPlanStepLog(
planmock.NewPlanStep("step-1", "success"),
planmock.NewPlanStepWithDetailsAndError("plan-completed", []*models.ClusterPlanStepLogMessageInfo{
{Message: ec.String("failure")},
}),
),
},
},
})),
),
),
wantErr: true,
},
{
name: "returns error when updated plan has error",
deploymentID: mockDeploymentID,
updatedTopology: NewTopologySize(512),
ecAPI: api.NewMock(
mock.New200Response(mock.NewStructBody(defaultMockDeploymentGetResponse)),
mock.New200Response(mock.NewStringBody(`{}`)), // updated
mock.New200Response(mock.NewStructBody(
planmock.Generate(planmock.GenerateConfig{
ID: mockDeploymentID,
Elasticsearch: []planmock.GeneratedResourceConfig{
{
ID: "cde7b6b605424a54ce9d56316eab13a1",
PendingLog: nil,
CurrentLog: planmock.NewPlanStepLog(
planmock.NewPlanStep("step-1", "success"),
planmock.NewPlanStep("plan-completed", "success"),
),
},
},
})),
),
mock.New200Response(mock.NewStructBody(
planmock.Generate(planmock.GenerateConfig{
ID: mockDeploymentID,
Elasticsearch: []planmock.GeneratedResourceConfig{
{
ID: "cde7b6b605424a54ce9d56316eab13a1",
PendingLog: nil,
CurrentLog: planmock.NewPlanStepLog(
planmock.NewPlanStep("step-1", "success"),
planmock.NewPlanStep("plan-completed", "success"),
),
},
},
})),
),
mock.New200Response(mock.NewStructBody(&models.DeploymentGetResponse{
Healthy: ec.Bool(false),
ID: ec.String(mock.ValidClusterID),
Resources: &models.DeploymentResources{
Elasticsearch: []*models.ElasticsearchResourceInfo{
{
Info: &models.ElasticsearchClusterInfo{
PlanInfo: &models.ElasticsearchClusterPlansInfo{
Current: &models.ElasticsearchClusterPlanInfo{
Error: &models.ClusterPlanAttemptError{
FailureType: "InfrastructureFailure:NotEnoughCapacity",
Message: ec.String("Plan change failed: Not enough capacity to allocate instance(s)"),
},
},
},
},
},
},
},
},
)),
),
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
c := &clientImpl{
ecAPI: tt.ecAPI,
deploymentID: tt.deploymentID,
}
err := c.UpdateESHotContentTopologySize(context.Background(), tt.updatedTopology)
if (err != nil) != tt.wantErr {
t.Errorf("UpdateESHotContentTopologySize() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
15 changes: 15 additions & 0 deletions pkg/elasticcloud/util.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package elasticcloud

import (
"fmt"
"github.com/elastic/cloud-sdk-go/pkg/models"
"github.com/k-yomo/elastic-cloud-autoscaler/pkg/memory"
"strings"
)

func FindHotContentTopology(topologies []*models.ElasticsearchClusterTopologyElement) *models.ElasticsearchClusterTopologyElement {
Expand All @@ -25,3 +27,16 @@ func CalcNodeNum(topologySize *models.TopologySize, zoneCount int32) int {
func CalcTopologyNodeNum(topology *models.ElasticsearchClusterTopologyElement) int {
return CalcNodeNum(topology.Size, topology.ZoneCount)
}

func formatPlanAttemptError(planAttemptError *models.ClusterPlanAttemptError) string {
var sb strings.Builder
fmt.Fprintf(&sb, "failureType: %s", planAttemptError.FailureType)
if planAttemptError.Message != nil {
fmt.Fprintf(&sb, ", message: %s", *planAttemptError.Message)
}
if planAttemptError.Timestamp != nil {
fmt.Fprintf(&sb, ", timestamp: %s", planAttemptError.Timestamp.String())
}
fmt.Fprintf(&sb, ", details: %v", planAttemptError.Details)
return sb.String()
}

0 comments on commit 12a8ae3

Please sign in to comment.