ec/ecresource/deploymentresource/elasticsearch/v2/node_roles.go (113 lines of code) (raw):
// 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 v2
import (
"context"
"fmt"
"github.com/blang/semver"
"github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/utils"
"github.com/elastic/terraform-provider-ec/ec/internal/planmodifiers"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
)
func CompatibleWithNodeRoles(version string) (bool, error) {
deploymentVersion, err := semver.Parse(version)
if err != nil {
return false, fmt.Errorf("failed to parse Elasticsearch version: %w", err)
}
return deploymentVersion.GE(utils.DataTiersVersion), nil
}
func UseNodeRoles(ctx context.Context, stateVersion, planVersion types.String, planElasticsearch types.Object) (bool, diag.Diagnostics) {
compatibleWithNodeRoles, err := CompatibleWithNodeRoles(planVersion.ValueString())
if err != nil {
var diags diag.Diagnostics
diags.AddError("Failed to determine whether to use node_roles", err.Error())
return false, diags
}
if !compatibleWithNodeRoles {
return false, nil
}
convertLegacy, diags := legacyToNodeRoles(ctx, stateVersion, planVersion, planElasticsearch)
if diags.HasError() {
return false, diags
}
return convertLegacy, nil
}
// legacyToNodeRoles returns true when the legacy "node_type_*" should be
// migrated over to node_roles. Which will be true when:
// * The version field doesn't change.
// * The version field changes but:
// - The Elasticsearch.0.toplogy doesn't have any node_type_* set.
func legacyToNodeRoles(ctx context.Context, stateVersion, planVersion types.String, planElasticsearch types.Object) (bool, diag.Diagnostics) {
if stateVersion.ValueString() == "" || stateVersion.ValueString() == planVersion.ValueString() {
return true, nil
}
var diags diag.Diagnostics
oldVersion, err := semver.Parse(stateVersion.ValueString())
if err != nil {
diags.AddError("Failed to parse previous Elasticsearch version", err.Error())
return false, diags
}
newVersion, err := semver.Parse(planVersion.ValueString())
if err != nil {
diags.AddError("Failed to parse new Elasticsearch version", err.Error())
return false, diags
}
// if the version change moves from non-node_roles to one
// that supports node roles, do not migrate on that step.
if oldVersion.LT(utils.DataTiersVersion) && newVersion.GE(utils.DataTiersVersion) {
return false, nil
}
// When any topology elements in the state have the node_type_*
// properties set, the node_role field cannot be used, since
// we'd be changing the version AND migrating over `node_role`s
// which is not permitted by the API.
hasNodeTypes, d := PlanHasNodeTypes(ctx, planElasticsearch)
diags.Append(d...)
return !hasNodeTypes, d
}
func PlanHasNodeTypes(ctx context.Context, planElasticsearch types.Object) (bool, diag.Diagnostics) {
var es *ElasticsearchTF
diags := tfsdk.ValueAs(ctx, planElasticsearch, &es)
if diags.HasError() {
return false, diags
}
if es == nil {
diags.AddError("Cannot determine if node types are defined", "cannot find elasticsearch object")
return false, diags
}
tiers, diags := es.topologies(ctx)
if diags.HasError() {
return false, diags
}
for _, tier := range tiers {
if tier != nil && tier.HasNodeTypes() {
return true, nil
}
}
return false, nil
}
// if useState is false, useNodeRoles is always false
func useStateAndNodeRolesInPlanModifiers(ctx context.Context, configValue attr.Value, plan tfsdk.Plan, state tfsdk.State, planValue attr.Value) (useState bool, useNodeRoles bool, diags diag.Diagnostics) {
if !planValue.IsUnknown() {
return false, false, nil
}
if configValue.IsUnknown() {
return false, false, nil
}
var stateVersion types.String
if diags := state.GetAttribute(ctx, path.Root("version"), &stateVersion); diags.HasError() {
return false, false, diags
}
// If resource has state, then it should contain version.
// So if there is no version in state, plan modifier is called for Create.
// In that case there is no state to use.
// We cannot use StateValue from request parameter for this purpose,
// because null can be a valid state for node_roles and node_types in Update.
// E.g. node_roles' state can be null if node_types are used.
if stateVersion.IsNull() {
return false, false, nil
}
// if template changed return
templateChanged, diags := planmodifiers.AttributeChanged(ctx, path.Root("deployment_template_id"), plan, state)
if diags.HasError() {
return false, false, diags
}
if templateChanged {
return false, false, nil
}
var planVersion types.String
if diags := plan.GetAttribute(ctx, path.Root("version"), &planVersion); diags.HasError() {
return false, false, diags
}
var elasticsearch types.Object
if diags := plan.GetAttribute(ctx, path.Root("elasticsearch"), &elasticsearch); diags.HasError() {
return false, false, diags
}
if useNodeRoles, diags = UseNodeRoles(ctx, stateVersion, planVersion, elasticsearch); diags.HasError() {
return false, false, diags
}
return true, useNodeRoles, nil
}