ec/internal/planmodifiers/use_state_unless_template_changed.go (108 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 planmodifiers
import (
"context"
"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/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
)
// UseStateForUnknownUnlessMigrationIsRequired Use current state for a topology's attribute, unless one of the following scenarios occurs:
// 1. The attribute is not nullable (`isNullable = false`) and the topology's state is nil
// 2. The deployment template attribute has changed
// 3. `migrate_to_latest_hardware` is set to `true` and there is a migration available to be performed
// 4. The state of the parent attribute is nil
func UseStateForUnknownUnlessMigrationIsRequired(resourceKind string, isNullable bool) useStateForUnknownUnlessMigrationIsRequired {
return useStateForUnknownUnlessMigrationIsRequired{resourceKind: resourceKind, isNullable: isNullable}
}
type useStateForUnknownUnlessMigrationIsRequired struct {
resourceKind string
isNullable bool
}
type PlanModifierResponse interface {
planmodifier.StringResponse | planmodifier.Int64Response
}
func (m useStateForUnknownUnlessMigrationIsRequired) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
useState, diags := m.UseState(ctx, req.ConfigValue, req.Plan, req.State, resp.PlanValue, req.StateValue)
resp.Diagnostics.Append(diags...)
if useState {
resp.PlanValue = req.StateValue
}
}
func (m useStateForUnknownUnlessMigrationIsRequired) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) {
useState, diags := m.UseState(ctx, req.ConfigValue, req.Plan, req.State, resp.PlanValue, req.StateValue)
resp.Diagnostics.Append(diags...)
if useState {
resp.PlanValue = req.StateValue
}
}
func (m useStateForUnknownUnlessMigrationIsRequired) UseState(ctx context.Context, configValue attr.Value, plan tfsdk.Plan, state tfsdk.State, planValue attr.Value, stateValue attr.Value) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
var parentResState attr.Value
if d := state.GetAttribute(ctx, path.Root(m.resourceKind), &parentResState); d.HasError() {
return false, d
}
resourceIsBeingCreated := parentResState.IsNull()
if resourceIsBeingCreated {
return false, nil
}
if stateValue.IsNull() && !m.isNullable {
return false, nil
}
if !planValue.IsUnknown() {
return false, nil
}
// if the config is the unknown value, use the unknown value otherwise, interpolation gets messed up
if configValue.IsUnknown() {
return false, nil
}
templateChanged, d := AttributeChanged(ctx, path.Root("deployment_template_id"), plan, state)
diags.Append(d...)
// If template changed, we won't use state
if templateChanged {
return false, diags
}
var migrateToLatestHw bool
plan.GetAttribute(ctx, path.Root("migrate_to_latest_hardware"), &migrateToLatestHw)
// If migrate_to_latest_hardware isn't set, we want to use state
if !migrateToLatestHw {
return true, diags
}
isMigrationAvailable, d := CheckAvailableMigration(ctx, plan, state, path.Root(m.resourceKind))
diags.Append(d...)
if diags.HasError() {
return false, diags
}
if isMigrationAvailable {
return false, diags
}
return true, diags
}
func (r useStateForUnknownUnlessMigrationIsRequired) Description(ctx context.Context) string {
return "Use tier's state if it's defined and template is the same."
}
func (r useStateForUnknownUnlessMigrationIsRequired) MarkdownDescription(ctx context.Context) string {
return "Use tier's state if it's defined and template is the same."
}
func diffStateAttributes(ctx context.Context, p1 path.Path, p2 path.Path, state tfsdk.State) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
var p1Value attr.Value
d1 := state.GetAttribute(ctx, p1, &p1Value)
diags.Append(d1...)
var p2Value attr.Value
d2 := state.GetAttribute(ctx, p2, &p2Value)
diags.Append(d2...)
return !p1Value.Equal(p2Value), diags
}
func attributePlanDefined(ctx context.Context, p path.Path, plan tfsdk.Plan) (bool, diag.Diagnostics) {
var value attr.Value
diags := plan.GetAttribute(ctx, p, &value)
return !value.IsNull() && !value.IsUnknown(), diags
}
func CheckAvailableMigration(ctx context.Context, plan tfsdk.Plan, state tfsdk.State, topologyPath path.Path) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
planHasInstanceConfigId, d := attributePlanDefined(ctx, topologyPath.AtName("instance_configuration_id"), plan)
diags.Append(d...)
planHasInstanceConfigVersion, d := attributePlanDefined(ctx, topologyPath.AtName("instance_configuration_version"), plan)
diags.Append(d...)
// We won't migrate this topology element if 'instance_configuration_id' or 'instance_configuration_version' are
// defined on the TF configuration. Otherwise, we may be setting an incorrect value for 'size', in case the
// template IC has different size increments
if planHasInstanceConfigId || planHasInstanceConfigVersion {
return false, diags
}
instanceConfigIdsDiff, d := diffStateAttributes(ctx, topologyPath.AtName("instance_configuration_id"), topologyPath.AtName("latest_instance_configuration_id"), state)
diags.Append(d...)
instanceConfigVersionsDiff, d := diffStateAttributes(ctx, topologyPath.AtName("instance_configuration_version"), topologyPath.AtName("latest_instance_configuration_version"), state)
diags.Append(d...)
// We consider that a migration is available when:
// * the current instance config ID doesn't match the one in the template
// * the instance config IDs match but the instance config versions differ
return instanceConfigIdsDiff || instanceConfigVersionsDiff, diags
}