Skip to content

Commit

Permalink
Support transitions to/from dedicated masters by setting hot tier nod…
Browse files Browse the repository at this point in the history
…e_roles to unknown on topology size changes (#682)
  • Loading branch information
tobio authored Aug 4, 2023
1 parent 22a9f50 commit f8bc0c4
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 25 deletions.
50 changes: 46 additions & 4 deletions ec/acc/deployment_dedicated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,39 @@ func TestAccDeployment_dedicated_coordinating(t *testing.T) {
func TestAccDeployment_dedicated_master(t *testing.T) {
resName := "ec_deployment.dedicated_master"
randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
startCfg := "testdata/deployment_dedicated_master.tf"
cfg := fixtureAccDeploymentResourceBasicDefaults(t, startCfg, randomName, getRegion(), hotWarmTemplate)
belowCfg := "testdata/deployment_dedicated_master_below_threshold.tf"
aboveCfg := "testdata/deployment_dedicated_master_above_threshold.tf"
belowThresholdCfg := fixtureAccDeploymentResourceBasicDefaults(t, belowCfg, randomName, getRegion(), defaultTemplate)
aboveThresholdCfg := fixtureAccDeploymentResourceBasicDefaults(t, aboveCfg, randomName, getRegion(), defaultTemplate)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProviderFactory,
CheckDestroy: testAccDeploymentDestroy,
Steps: []resource.TestStep{
{
// Create a deployment with dedicated master nodes.
Config: cfg,
// Create a deployment below the dedicate master threshold.
Config: belowThresholdCfg,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet(resName, "elasticsearch.hot.instance_configuration_id"),

resource.TestCheckNoResourceAttr(resName, "elasticsearch.cold"),
resource.TestCheckNoResourceAttr(resName, "elasticsearch.master"),
resource.TestCheckNoResourceAttr(resName, "elasticsearch.warm"),

resource.TestCheckResourceAttr(resName, "elasticsearch.hot.size", "1g"),
resource.TestCheckResourceAttr(resName, "elasticsearch.hot.size_resource", "memory"),
resource.TestCheckResourceAttrSet(resName, "elasticsearch.hot.node_roles.#"),
resource.TestCheckResourceAttr(resName, "elasticsearch.hot.zone_count", "3"),

resource.TestCheckNoResourceAttr(resName, "kibana"),
resource.TestCheckNoResourceAttr(resName, "apm"),
resource.TestCheckNoResourceAttr(resName, "enterprise_search"),
),
},
{
// Expand the deployment above the dedicated master threshold.
Config: aboveThresholdCfg,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet(resName, "elasticsearch.cold.instance_configuration_id"),
resource.TestCheckResourceAttrSet(resName, "elasticsearch.hot.instance_configuration_id"),
Expand All @@ -106,6 +128,26 @@ func TestAccDeployment_dedicated_master(t *testing.T) {
resource.TestCheckResourceAttrSet(resName, "elasticsearch.warm.node_roles.#"),
resource.TestCheckResourceAttr(resName, "elasticsearch.warm.zone_count", "2"),

resource.TestCheckNoResourceAttr(resName, "kibana"),
resource.TestCheckNoResourceAttr(resName, "apm"),
resource.TestCheckNoResourceAttr(resName, "enterprise_search"),
),
},
{
// Shrink it back below the threshold.
Config: belowThresholdCfg,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet(resName, "elasticsearch.hot.instance_configuration_id"),

resource.TestCheckNoResourceAttr(resName, "elasticsearch.cold"),
resource.TestCheckNoResourceAttr(resName, "elasticsearch.master"),
resource.TestCheckNoResourceAttr(resName, "elasticsearch.warm"),

resource.TestCheckResourceAttr(resName, "elasticsearch.hot.size", "1g"),
resource.TestCheckResourceAttr(resName, "elasticsearch.hot.size_resource", "memory"),
resource.TestCheckResourceAttrSet(resName, "elasticsearch.hot.node_roles.#"),
resource.TestCheckResourceAttr(resName, "elasticsearch.hot.zone_count", "3"),

resource.TestCheckNoResourceAttr(resName, "kibana"),
resource.TestCheckNoResourceAttr(resName, "apm"),
resource.TestCheckNoResourceAttr(resName, "enterprise_search"),
Expand Down
19 changes: 19 additions & 0 deletions ec/acc/testdata/deployment_dedicated_master_below_threshold.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
data "ec_stack" "latest" {
version_regex = "latest"
region = "%s"
}

resource "ec_deployment" "dedicated_master" {
name = "%s"
region = "%s"
version = data.ec_stack.latest.version
deployment_template_id = "%s"

elasticsearch = {
hot = {
zone_count = 3
size = "1g"
autoscaling = {}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ package v2
import (
"context"

"github.com/hashicorp/terraform-plugin-framework/types"

"github.com/elastic/terraform-provider-ec/ec/internal/planmodifiers"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)

Expand Down Expand Up @@ -70,3 +74,41 @@ func (r nodeRolesDefault) Description(ctx context.Context) string {
func (r nodeRolesDefault) MarkdownDescription(ctx context.Context) string {
return "Use current state if it's still valid."
}

type setUnknownOnTopologyChanges struct{}

var (
tierNames = []string{"hot", "coordinating", "master", "warm", "cold", "frozen"}
sizingAttributes = []string{"size", "zone_count"}
)

func (m setUnknownOnTopologyChanges) PlanModifySet(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) {
if req.PlanValue.IsUnknown() || req.PlanValue.IsNull() {
return
}

for _, tierName := range tierNames {
for _, attr := range sizingAttributes {
hasChanged, diags := planmodifiers.AttributeChanged(ctx, path.Root("elasticsearch").AtName(tierName).AtName(attr), req.Plan, req.State)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

if hasChanged {
resp.PlanValue = types.SetUnknown(types.StringType)
return
}
}
}
}

// Description returns a human-readable description of the plan modifier.
func (r setUnknownOnTopologyChanges) Description(ctx context.Context) string {
return "Sets the plan value to unknown if the size of any topology element has changed."
}

// MarkdownDescription returns a markdown description of the plan modifier.
func (r setUnknownOnTopologyChanges) MarkdownDescription(ctx context.Context) string {
return "Sets the plan value to unknown if the size of any topology element has changed."
}
71 changes: 50 additions & 21 deletions ec/ecresource/deploymentresource/elasticsearch/v2/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package v2

import (
"fmt"
"strings"

"github.com/elastic/terraform-provider-ec/ec/internal/planmodifiers"
Expand Down Expand Up @@ -94,13 +95,29 @@ func ElasticsearchSchema() schema.Attribute {
Computed: true,
},

"hot": elasticsearchTopologySchema("'hot' topology element", true, "hot"),
"coordinating": elasticsearchTopologySchema("'coordinating' topology element", false, "coordinating"),
"master": elasticsearchTopologySchema("'master' topology element", false, "master"),
"warm": elasticsearchTopologySchema("'warm' topology element", false, "warm"),
"cold": elasticsearchTopologySchema("'cold' topology element", false, "cold"),
"frozen": elasticsearchTopologySchema("'frozen' topology element", false, "frozen"),
"ml": elasticsearchTopologySchema("'ml' topology element", false, "ml"),
"hot": elasticsearchTopologySchema(topologySchemaOptions{
tierName: "hot",
required: true,
nodeRolesImpactedBySizeChange: true,
}),
"coordinating": elasticsearchTopologySchema(topologySchemaOptions{
tierName: "coordinating",
}),
"master": elasticsearchTopologySchema(topologySchemaOptions{
tierName: "master",
}),
"warm": elasticsearchTopologySchema(topologySchemaOptions{
tierName: "warm",
}),
"cold": elasticsearchTopologySchema(topologySchemaOptions{
tierName: "cold",
}),
"frozen": elasticsearchTopologySchema(topologySchemaOptions{
tierName: "frozen",
}),
"ml": elasticsearchTopologySchema(topologySchemaOptions{
tierName: "ml",
}),

"trust_account": elasticsearchTrustAccountSchema(),

Expand Down Expand Up @@ -402,27 +419,41 @@ func elasticsearchTrustExternalSchema() schema.Attribute {
}
}

func elasticsearchTopologySchema(description string, required bool, topologyAttributeName string) schema.Attribute {
type topologySchemaOptions struct {
required bool
nodeRolesImpactedBySizeChange bool
tierName string
}

func elasticsearchTopologySchema(options topologySchemaOptions) schema.Attribute {
nodeRolesPlanModifiers := []planmodifier.Set{
UseNodeRolesDefault(),
}

if options.nodeRolesImpactedBySizeChange {
nodeRolesPlanModifiers = append(nodeRolesPlanModifiers, setUnknownOnTopologyChanges{})
}

return schema.SingleNestedAttribute{
Optional: !required,
Optional: !options.required,
// it should be Computed but Computed triggers TF weird behaviour that leads to unempty plan for zero change config
// Computed: true,
Required: required,
Description: description,
Required: options.required,
Description: fmt.Sprintf("'%s' topology element", options.tierName),
Attributes: map[string]schema.Attribute{
"instance_configuration_id": schema.StringAttribute{
Description: `Computed Instance Configuration ID of the topology element`,
Computed: true,
PlanModifiers: []planmodifier.String{
UseTopologyStateForUnknown(topologyAttributeName),
UseTopologyStateForUnknown(options.tierName),
},
},
"size": schema.StringAttribute{
Description: `Amount of "size_resource" per node in the "<size in GB>g" notation`,
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.String{
UseTopologyStateForUnknown(topologyAttributeName),
UseTopologyStateForUnknown(options.tierName),
},
},
"size_resource": schema.StringAttribute{
Expand All @@ -438,7 +469,7 @@ func elasticsearchTopologySchema(description string, required bool, topologyAttr
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.Int64{
UseTopologyStateForUnknown(topologyAttributeName),
UseTopologyStateForUnknown(options.tierName),
},
},
"node_type_data": schema.StringAttribute{
Expand Down Expand Up @@ -474,14 +505,12 @@ func elasticsearchTopologySchema(description string, required bool, topologyAttr
},
},
"node_roles": schema.SetAttribute{
ElementType: types.StringType,
Description: `The computed list of node roles for the current topology element`,
Computed: true,
PlanModifiers: []planmodifier.Set{
UseNodeRolesDefault(),
},
ElementType: types.StringType,
Description: `The computed list of node roles for the current topology element`,
Computed: true,
PlanModifiers: nodeRolesPlanModifiers,
},
"autoscaling": elasticsearchTopologyAutoscalingSchema(topologyAttributeName),
"autoscaling": elasticsearchTopologyAutoscalingSchema(options.tierName),
},
}
}

0 comments on commit f8bc0c4

Please sign in to comment.