ec/ecresource/deploymentresource/topology_dedicated_masters.go (315 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 deploymentresource
import (
"context"
"fmt"
"github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/deploymentsize"
"github.com/elastic/cloud-sdk-go/pkg/models"
es "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/elasticsearch/v2"
"github.com/elastic/terraform-provider-ec/ec/internal/util"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var objectAsOptions = basetypes.ObjectAsOptions{UnhandledNullAsEmpty: false, UnhandledUnknownAsEmpty: false}
func UpdateDedicatedMasterTier(
ctx context.Context,
config tfsdk.Config,
plan tfsdk.Plan,
privateState PrivateState,
resp *resource.ModifyPlanResponse,
loadTemplate func() (*models.DeploymentTemplateInfoV2, error),
) {
var esConfig es.ElasticsearchTF
resp.Diagnostics.Append(config.GetAttribute(ctx, path.Root("elasticsearch"), &esConfig)...)
if resp.Diagnostics.HasError() {
return
}
if !esConfig.MasterTier.IsNull() && !esConfig.MasterTier.IsUnknown() {
// Master tier is explicitly configured -> No changes will be made
tflog.Debug(ctx, "Skip UpdateDedicatedMasterTier: Master tier has been explicitly configured")
return
}
var planElasticsearch es.ElasticsearchTF
resp.Diagnostics.Append(plan.GetAttribute(ctx, path.Root("elasticsearch"), &planElasticsearch)...)
if resp.Diagnostics.HasError() {
return
}
template, err := loadTemplate()
if err != nil {
resp.Diagnostics.AddError("Failed to get deployment-template", "Error: "+err.Error())
return
}
dedicatedMastersThreshold := getDedicatedMastersThreshold(*template)
if dedicatedMastersThreshold == 0 {
// No automatic dedicated masters management
return
}
nodesInCluster := countNodesInCluster(ctx, planElasticsearch, *template)
if nodesInCluster < dedicatedMastersThreshold {
disableMasterTier(ctx, planElasticsearch, resp)
} else {
enableMasterTier(ctx, privateState, template, plan, planElasticsearch, resp)
}
}
func disableMasterTier(ctx context.Context, planElasticsearch es.ElasticsearchTF, resp *resource.ModifyPlanResponse) {
if planElasticsearch.MasterTier.IsUnknown() || planElasticsearch.MasterTier.IsNull() {
resp.Plan.SetAttribute(ctx,
path.Root("elasticsearch").AtName("master"),
types.ObjectNull(es.ElasticsearchTopologyAttrs()),
)
return
}
resp.Plan.SetAttribute(ctx,
path.Root("elasticsearch").AtName("master").AtName("size"),
"0g",
)
}
func enableMasterTier(
ctx context.Context,
privateState PrivateState,
template *models.DeploymentTemplateInfoV2,
plan tfsdk.Plan,
planElasticsearch es.ElasticsearchTF,
resp *resource.ModifyPlanResponse,
) {
var migrateToLatestHw bool
plan.GetAttribute(ctx, path.Root("migrate_to_latest_hardware"), &migrateToLatestHw)
// Skip update if the master tier is already enabled
// If migrateToLatestHw is true, update the tier to values from latest IC
if masterTierIsEnabled(ctx, planElasticsearch, *template) && !migrateToLatestHw {
return
}
instanceConfiguration := selectInstanceConfigurationToUse(ctx, privateState, template, planElasticsearch, migrateToLatestHw)
if instanceConfiguration == nil {
return
}
zones := instanceConfiguration.MaxZones
resp.Plan.SetAttribute(ctx,
path.Root("elasticsearch").AtName("master").AtName("zone_count"),
zones,
)
defaultSize := util.MemoryToState(instanceConfiguration.DiscreteSizes.DefaultSize)
resp.Plan.SetAttribute(ctx,
path.Root("elasticsearch").AtName("master").AtName("size"),
defaultSize,
)
}
func masterTierIsEnabled(ctx context.Context, planElasticsearch es.ElasticsearchTF, template models.DeploymentTemplateInfoV2) bool {
if planElasticsearch.MasterTier.IsUnknown() || planElasticsearch.MasterTier.IsNull() {
return false
}
size, zoneCount := getSizeAndZoneCount(ctx, "master", planElasticsearch.MasterTier, template)
return size > 0 && zoneCount > 0
}
func selectInstanceConfigurationToUse(
ctx context.Context,
privateState PrivateState,
template *models.DeploymentTemplateInfoV2,
planElasticsearch es.ElasticsearchTF,
migrateToLatestHw bool,
) *models.InstanceConfigurationInfo {
templateInstanceConfig := getTemplateInstanceConfiguration(*template, "master")
if migrateToLatestHw {
// Since we will migrate to the latest IC version, use latest IC stored in template
return templateInstanceConfig
}
instanceConfigurations, diags := ReadPrivateStateInstanceConfigurations(ctx, privateState)
if diags.HasError() {
tflog.Debug(ctx, "selectInstanceConfigurationToUse: Failed to read instance-configs from private state", withDiags(diags))
return nil
}
instanceConfiguration := getInstanceConfiguration(ctx, planElasticsearch.MasterTier, instanceConfigurations)
if instanceConfiguration == nil || instanceConfiguration.DiscreteSizes == nil {
// Fall back to template IC
instanceConfiguration = templateInstanceConfig
}
if instanceConfiguration == nil || instanceConfiguration.DiscreteSizes == nil {
tflog.Debug(ctx, "selectInstanceConfigurationToUse: No instance-config for master tier.")
return nil
}
if instanceConfiguration.MaxZones == 0 {
// In case the max-zones are not set, fall back to template value
instanceConfiguration.MaxZones = templateInstanceConfig.MaxZones
}
return instanceConfiguration
}
func getDedicatedMastersThreshold(template models.DeploymentTemplateInfoV2) int32 {
dedicatedMastersThreshold := int32(0)
if template.DeploymentTemplate != nil && template.DeploymentTemplate.Resources != nil {
for _, e := range template.DeploymentTemplate.Resources.Elasticsearch {
if e.Settings == nil {
continue
}
dedicatedMastersThreshold = e.Settings.DedicatedMastersThreshold
break
}
}
return dedicatedMastersThreshold
}
func countNodesInCluster(ctx context.Context, esPlan es.ElasticsearchTF, template models.DeploymentTemplateInfoV2) int32 {
nodesInDeployment := int32(0)
tierTopologyIds := []string{"hot_content", "coordinating", "warm", "cold", "frozen"}
for _, topologyId := range tierTopologyIds {
var rawTopology types.Object
switch topologyId {
case "hot_content":
rawTopology = esPlan.HotContentTier
case "coordinating":
rawTopology = esPlan.CoordinatingTier
case "warm":
rawTopology = esPlan.WarmTier
case "cold":
rawTopology = esPlan.ColdTier
case "frozen":
rawTopology = esPlan.FrozenTier
}
if rawTopology.IsNull() || rawTopology.IsUnknown() {
continue
}
size, zoneCount := getSizeAndZoneCount(ctx, topologyId, rawTopology, template)
// Calculate if there are >1 nodes in each zone:
// If the size is > the maximum size allowed in the zone, more nodes are added to reach the desired size.
instanceConfig := getTemplateInstanceConfiguration(template, topologyId)
maxSize := getMaxSize(instanceConfig)
var nodesPerZone int32
if size < maxSize || maxSize == 0 {
nodesPerZone = 1
} else {
nodesPerZone = size / maxSize
}
if size > 0 && zoneCount > 0 {
nodesInDeployment += zoneCount * nodesPerZone
}
}
return nodesInDeployment
}
func getSizeAndZoneCount(
ctx context.Context,
topologyId string,
rawTopology types.Object,
template models.DeploymentTemplateInfoV2,
) (int32, int32) {
var topology es.ElasticsearchTopologyTF
diags := rawTopology.As(ctx, &topology, objectAsOptions)
if diags.HasError() {
tflog.Debug(ctx, "getSizeAndZoneCount: Failed to read topology.", withDiags(diags))
return 0, 0
}
var size int32
if !topology.Size.IsUnknown() && !topology.Size.IsNull() {
var err error
size, err = deploymentsize.ParseGb(topology.Size.ValueString())
if err != nil {
tflog.Debug(ctx, "getSizeAndZoneCount: Failed to parse topology size.", withError(err))
return 0, 0
}
} else {
// Fall back to template value
size = getTopologySize(template, topologyId)
}
var zoneCount int32
if !topology.ZoneCount.IsUnknown() && !topology.ZoneCount.IsNull() {
zoneCount = int32(topology.ZoneCount.ValueInt64())
} else {
// Fall back to template value
zoneCount = getTopologyZoneCount(template, topologyId)
}
return size, zoneCount
}
// If the instance-config-id is set in the topology, loads that specific IC via API
// If no instance-config-id is set, uses the IC set in the deployment template
func getInstanceConfiguration(
ctx context.Context,
rawTopology types.Object,
deploymentInstanceConfigs []models.InstanceConfigurationInfo,
) *models.InstanceConfigurationInfo {
if rawTopology.IsUnknown() || rawTopology.IsNull() {
return nil
}
var topology es.ElasticsearchTopologyTF
diags := rawTopology.As(ctx, &topology, objectAsOptions)
if diags.HasError() {
tflog.Debug(ctx, "getInstanceConfigurationId: Failed to read topology.", withDiags(diags))
return nil
}
if topology.InstanceConfigurationId.IsUnknown() || topology.InstanceConfigurationId.IsNull() {
return nil
}
icId := topology.InstanceConfigurationId.ValueStringPointer()
icConfigVersion := int32(topology.InstanceConfigurationVersion.ValueInt64())
if icId == nil || *icId == "" {
return nil
}
var deploymentIc *models.InstanceConfigurationInfo
for _, ic := range deploymentInstanceConfigs {
if ic.ID == *icId && ic.ConfigVersion == icConfigVersion {
deploymentIc = &ic
break
}
}
if deploymentIc == nil {
tflog.Debug(ctx, fmt.Sprintf("UpdateDedicatedMasterTier: Instance-config not found: %s", *icId))
return nil
}
return deploymentIc
}
func getTemplateInstanceConfiguration(template models.DeploymentTemplateInfoV2, topologyId string) *models.InstanceConfigurationInfo {
if template.DeploymentTemplate == nil ||
template.DeploymentTemplate.Resources == nil ||
len(template.DeploymentTemplate.Resources.Elasticsearch) == 0 ||
template.DeploymentTemplate.Resources.Elasticsearch[0].Plan == nil {
return nil
}
var topologyElement *models.ElasticsearchClusterTopologyElement
for _, topology := range template.DeploymentTemplate.Resources.Elasticsearch[0].Plan.ClusterTopology {
if topology.ID == topologyId {
topologyElement = topology
break
}
}
if topologyElement == nil {
return nil
}
// Find IC for tier
for _, ic := range template.InstanceConfigurations {
if ic.ID == topologyElement.InstanceConfigurationID {
return ic
}
}
return nil
}
func getMaxSize(ic *models.InstanceConfigurationInfo) int32 {
if ic == nil || ic.DiscreteSizes == nil {
return 0
}
maxSize := int32(0)
for _, size := range ic.DiscreteSizes.Sizes {
if size > maxSize {
maxSize = size
}
}
return maxSize
}
func getTopologySize(template models.DeploymentTemplateInfoV2, tier string) int32 {
if len(template.DeploymentTemplate.Resources.Elasticsearch) == 0 {
return 0
}
elasticsearch := template.DeploymentTemplate.Resources.Elasticsearch[0]
for _, topology := range elasticsearch.Plan.ClusterTopology {
if topology.ID == tier {
if topology.Size == nil || topology.Size.Value == nil {
return 0
}
return *topology.Size.Value
}
}
return 0
}
func getTopologyZoneCount(template models.DeploymentTemplateInfoV2, tier string) int32 {
if len(template.DeploymentTemplate.Resources.Elasticsearch) == 0 {
return 0
}
elasticsearch := template.DeploymentTemplate.Resources.Elasticsearch[0]
for _, topology := range elasticsearch.Plan.ClusterTopology {
if topology.ID == tier {
return topology.ZoneCount
}
}
return 0
}
func withError(err error) map[string]interface{} {
return map[string]interface{}{"error": err}
}
func withDiags(diags diag.Diagnostics) map[string]interface{} {
return map[string]interface{}{"error": diags.Errors()}
}