Skip to content

Commit

Permalink
Automatically add and remove dedicated masters.
Browse files Browse the repository at this point in the history
If no master tier is configured, it is automatically added/removed:
- Added if the number of nodes is >= the dedicated master threshold
- Removed if the number of nodes is < the dedicated master threshold
  • Loading branch information
gigerdo committed Apr 25, 2024
1 parent e9ebedd commit 93b0677
Show file tree
Hide file tree
Showing 10 changed files with 686 additions and 11 deletions.
75 changes: 75 additions & 0 deletions ec/acc/deployment_add_dedicated_master_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package acc

import (
"fmt"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestAccDeployment_add_dedicated_master(t *testing.T) {
resName := "ec_deployment.basic"
randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
randomAlias := "alias" + acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

cfg5nodes := buildConfiguration(t, "testdata/deployment_dedicated_master_5_nodes.tf", randomAlias, randomName, getRegion(), defaultTemplate)
cfg6nodes := buildConfiguration(t, "testdata/deployment_dedicated_master_6_nodes.tf", randomAlias, randomName, getRegion(), defaultTemplate)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProviderFactory,
CheckDestroy: testAccDeploymentDestroy,
Steps: []resource.TestStep{
{
Config: cfg6nodes,
Check: resource.ComposeAggregateTestCheckFunc(
// Master tier should be enabled
resource.TestCheckResourceAttr(resName, "elasticsearch.master.size", "4g"),
resource.TestCheckResourceAttr(resName, "elasticsearch.master.zone_count", "3"),
),
},
{
Config: cfg5nodes,
Check: resource.ComposeAggregateTestCheckFunc(
// Master tier should be disabled
resource.TestCheckResourceAttr(resName, "elasticsearch.master.size", "0g"),
resource.TestCheckResourceAttr(resName, "elasticsearch.master.zone_count", "0"),
),
},
},
})
}

func buildConfiguration(t *testing.T, fileName, alias, name, region, depTpl string) string {
t.Helper()
requiresAPIConn(t)

deploymentTpl := setDefaultTemplate(region, depTpl)

b, err := os.ReadFile(fileName)
if err != nil {
t.Fatal(err)
}
return fmt.Sprintf(string(b),
region, alias, name, region, deploymentTpl,
)
}
28 changes: 28 additions & 0 deletions ec/acc/testdata/deployment_dedicated_master_5_nodes.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
data "ec_stack" "latest" {
version_regex = "latest"
region = "%s"
}

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

elasticsearch = {
hot = {
size = "2g"
zone_count = 2
autoscaling = {}
}

warm = {
size = "2g"
zone_count = 3
autoscaling = {}
}
}

kibana = {}
}
28 changes: 28 additions & 0 deletions ec/acc/testdata/deployment_dedicated_master_6_nodes.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
data "ec_stack" "latest" {
version_regex = "latest"
region = "%s"
}

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

elasticsearch = {
hot = {
size = "2g"
zone_count = 3
autoscaling = {}
}

warm = {
size = "2g"
zone_count = 3
autoscaling = {}
}
}

kibana = {}
}
18 changes: 15 additions & 3 deletions ec/ecresource/deploymentresource/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ func Test_createDeploymentWithEmptyFields(t *testing.T) {
r.UnitTest(t, r.TestCase{
ProtoV6ProviderFactories: protoV6ProviderFactoriesWithMockClient(
api.NewMock(
getTemplate(t, templateFileName),
getTemplate(t, templateFileName, true),
getTemplate(t, templateFileName, true),
getTemplate(t, templateFileName, true),
getTemplate(t, templateFileName, false),
createDeployment(t, readFile(t, "testdata/aws-io-optimized-v2-empty-config-create-expected-payload.json"), createDeploymentResponseJson, requestId),
mock.New200Response(readTestData(t, "testdata/aws-io-optimized-v2-empty-config-expected-deployment1.json")),
mock.New200Response(readTestData(t, "testdata/aws-io-optimized-v2-empty-config-expected-deployment2.json")),
Expand All @@ -97,9 +100,12 @@ func Test_createDeploymentWithEmptyFields(t *testing.T) {
mock.New200Response(readTestData(t, "testdata/aws-io-optimized-v2-empty-config-expected-deployment3.json")),
readRemoteClusters(t),
mock.New200Response(readTestData(t, "testdata/aws-io-optimized-v2-template-migration-response.json")),
getTemplate(t, templateFileName, true),
mock.New200Response(readTestData(t, "testdata/aws-io-optimized-v2-empty-config-expected-deployment3.json")),
readRemoteClusters(t),
mock.New200Response(readTestData(t, "testdata/aws-io-optimized-v2-template-migration-response.json")),
getTemplate(t, templateFileName, true),
getTemplate(t, templateFileName, true),
shutdownDeployment(t),
),
),
Expand All @@ -111,14 +117,20 @@ func Test_createDeploymentWithEmptyFields(t *testing.T) {
})
}

func getTemplate(t *testing.T, filename string) mock.Response {
func getTemplate(t *testing.T, filename string, withICs bool) mock.Response {
var query url.Values
if withICs {
query = url.Values{"region": {"us-east-1"}, "show_instance_configurations": {"true"}, "show_max_zones": {"true"}}
} else {
query = url.Values{"region": {"us-east-1"}, "show_instance_configurations": {"false"}, "show_max_zones": {"false"}}
}
return mock.New200ResponseAssertion(
&mock.RequestAssertion{
Host: api.DefaultMockHost,
Header: api.DefaultReadMockHeaders,
Method: "GET",
Path: "/api/v1/deployments/templates/aws-io-optimized-v2",
Query: url.Values{"region": {"us-east-1"}, "show_instance_configurations": {"false"}},
Query: query,
},
readTestData(t, filename),
)
Expand Down
20 changes: 15 additions & 5 deletions ec/ecresource/deploymentresource/elasticsearch/v2/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package v2

import (
"fmt"
"github.com/hashicorp/terraform-plugin-framework/attr"
"strings"

"github.com/elastic/terraform-provider-ec/ec/internal/planmodifiers"
Expand Down Expand Up @@ -483,12 +484,17 @@ func elasticsearchTopologySchema(options topologySchemaOptions) schema.Attribute
nodeRolesPlanModifiers = append(nodeRolesPlanModifiers, SetUnknownOnTopologySizeChange())
}

var topologyPlanModifiers []planmodifier.Object
if options.tierName == "master" {
topologyPlanModifiers = append(topologyPlanModifiers, objectplanmodifier.UseStateForUnknown())
}

return schema.SingleNestedAttribute{
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: options.required,
Description: fmt.Sprintf("'%s' topology element", options.tierName),
Optional: !options.required,
Computed: options.tierName == "master",
Required: options.required,
Description: fmt.Sprintf("'%s' topology element", options.tierName),
PlanModifiers: topologyPlanModifiers,
Attributes: map[string]schema.Attribute{
"instance_configuration_id": schema.StringAttribute{
Description: `Instance Configuration ID of the topology element`,
Expand Down Expand Up @@ -601,3 +607,7 @@ func elasticsearchTopologySchema(options topologySchemaOptions) schema.Attribute
},
}
}

func ElasticsearchTopologyAttrs() map[string]attr.Type {
return elasticsearchTopologySchema(topologySchemaOptions{}).GetType().(attr.TypeWithAttributeTypes).AttributeTypes()
}
58 changes: 58 additions & 0 deletions ec/ecresource/deploymentresource/plan_modifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package deploymentresource

import (
"context"
"github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/deptemplateapi"
deploymentv2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/deployment/v2"
"github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/planmodifiers"
"github.com/hashicorp/terraform-plugin-framework/resource"
)

var _ resource.ResourceWithModifyPlan = &Resource{}

func (r Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
if req.Plan.Raw.IsNull() {
// Resource is being destroyed
return
}

var plan deploymentv2.DeploymentTF
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

template, err := deptemplateapi.Get(deptemplateapi.GetParams{
API: r.client,
TemplateID: plan.DeploymentTemplateId.ValueString(),
Region: plan.Region.ValueString(),
HideInstanceConfigurations: false,
ShowMaxZones: true,
})
if err != nil {
resp.Diagnostics.AddError("Failed to get deployment-template", err.Error())
return
}

planmodifiers.UpdateDedicatedMasterTier(ctx, req, resp, *template)
if resp.Diagnostics.HasError() {
return
}
}
Loading

0 comments on commit 93b0677

Please sign in to comment.