Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically add and remove dedicated masters. #814

Merged
merged 19 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
90 changes: 90 additions & 0 deletions ec/acc/deployment_add_dedicated_master_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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 (
"errors"
"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.auto_dedicated_master"
randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)

cfg5nodes := buildConfiguration(t, "testdata/deployment_dedicated_master_5_nodes.tf", randomName, getRegion(), defaultTemplate)
cfg6nodes := buildConfiguration(t, "testdata/deployment_dedicated_master_6_nodes.tf", 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.TestCheckResourceAttrWith(
resName,
"elasticsearch.master.size",
func(v string) error {
if v == "0g" || v == "" {
return errors.New("master size should not be empty. size=" + v)
}
return nil
}),
resource.TestCheckResourceAttrWith(
resName,
"elasticsearch.master.zone_count",
func(v string) error {
if v == "0" || v == "" {
return errors.New("master zone_count should not be empty. zone_count=" + v)
}
return nil
}),
),
},
{
Config: cfg5nodes,
Check: resource.ComposeAggregateTestCheckFunc(
// Master tier should be disabled
resource.TestCheckNoResourceAttr(resName, "elasticsearch.master"),
),
},
},
})
}

func buildConfiguration(t *testing.T, fileName, 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, name, region, deploymentTpl,
)
}
27 changes: 27 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,27 @@
data "ec_stack" "latest" {
version_regex = "latest"
region = "%s"
}

resource "ec_deployment" "auto_dedicated_master" {
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 = {}
}
27 changes: 27 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,27 @@
data "ec_stack" "latest" {
version_regex = "latest"
region = "%s"
}

resource "ec_deployment" "auto_dedicated_master" {
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
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ func (m setUnknownOnTopologyChanges) PlanModifySet(ctx context.Context, req plan
}

for _, tierName := range tierNames {
var tierValue attr.Value
resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("elasticsearch").AtName(tierName), &tierValue)...)
if resp.Diagnostics.HasError() {
return
}

for _, attrName := range sizingAttributes {
attrPath := path.Root("elasticsearch").AtName(tierName).AtName(attrName)
var planValue attr.Value
Expand All @@ -110,7 +116,9 @@ func (m setUnknownOnTopologyChanges) PlanModifySet(ctx context.Context, req plan

// If the plan value is unknown then planmodifiers haven't run for this topology element
// Eventually the plan value will be set to the state value and it will be unchanged.
if planValue.IsUnknown() && !(stateValue.IsUnknown() || stateValue.IsNull()) {
// The tier should be directly checked for unknown, since the planValue will be null in that case (instead of unknown).
// See: https://github.com/hashicorp/terraform-plugin-framework/issues/186
if (planValue.IsUnknown() || tierValue.IsUnknown()) && !(stateValue.IsUnknown() || stateValue.IsNull()) {
continue
}

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())
gigerdo marked this conversation as resolved.
Show resolved Hide resolved
}

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",
dimuon marked this conversation as resolved.
Show resolved Hide resolved
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{
gigerdo marked this conversation as resolved.
Show resolved Hide resolved
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
Loading