ec/ecresource/deploymentresource/elasticsearch/v2/elasticsearch_topology.go (366 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" "encoding/json" "fmt" "github.com/elastic/cloud-sdk-go/pkg/client/deployments" "reflect" "strconv" "strings" "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/deploymentsize" "github.com/elastic/cloud-sdk-go/pkg/models" "github.com/elastic/cloud-sdk-go/pkg/util/ec" v1 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/elasticsearch/v1" "github.com/elastic/terraform-provider-ec/ec/internal/converters" "github.com/elastic/terraform-provider-ec/ec/internal/util" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) type ElasticsearchTopologyTF struct { InstanceConfigurationId types.String `tfsdk:"instance_configuration_id"` LatestInstanceConfigurationId types.String `tfsdk:"latest_instance_configuration_id"` InstanceConfigurationVersion types.Int64 `tfsdk:"instance_configuration_version"` LatestInstanceConfigurationVersion types.Int64 `tfsdk:"latest_instance_configuration_version"` Size types.String `tfsdk:"size"` SizeResource types.String `tfsdk:"size_resource"` ZoneCount types.Int64 `tfsdk:"zone_count"` NodeTypeData types.String `tfsdk:"node_type_data"` NodeTypeMaster types.String `tfsdk:"node_type_master"` NodeTypeIngest types.String `tfsdk:"node_type_ingest"` NodeTypeMl types.String `tfsdk:"node_type_ml"` NodeRoles types.Set `tfsdk:"node_roles"` Autoscaling types.Object `tfsdk:"autoscaling"` } type ElasticsearchTopology struct { id string InstanceConfigurationId *string `tfsdk:"instance_configuration_id"` LatestInstanceConfigurationId *string `tfsdk:"latest_instance_configuration_id"` InstanceConfigurationVersion *int `tfsdk:"instance_configuration_version"` LatestInstanceConfigurationVersion *int `tfsdk:"latest_instance_configuration_version"` Size *string `tfsdk:"size"` SizeResource *string `tfsdk:"size_resource"` ZoneCount int `tfsdk:"zone_count"` NodeTypeData *string `tfsdk:"node_type_data"` NodeTypeMaster *string `tfsdk:"node_type_master"` NodeTypeIngest *string `tfsdk:"node_type_ingest"` NodeTypeMl *string `tfsdk:"node_type_ml"` NodeRoles []string `tfsdk:"node_roles"` Autoscaling *ElasticsearchTopologyAutoscaling `tfsdk:"autoscaling"` } type ElasticsearchTopologyAutoscaling v1.ElasticsearchTopologyAutoscaling func (topology ElasticsearchTopologyTF) payload(ctx context.Context, topologyID string, planTopologies []*models.ElasticsearchClusterTopologyElement) diag.Diagnostics { var diags diag.Diagnostics topologyElem, err := matchEsTopologyID(topologyID, planTopologies) if err != nil { diags.AddError("topology matching error", err.Error()) return diags } if topology.InstanceConfigurationId.ValueString() != "" { topologyElem.InstanceConfigurationID = topology.InstanceConfigurationId.ValueString() } if !(topology.InstanceConfigurationVersion.IsUnknown() || topology.InstanceConfigurationVersion.IsNull()) { topologyElem.InstanceConfigurationVersion = ec.Int32(int32(topology.InstanceConfigurationVersion.ValueInt64())) } size, err := converters.ParseTopologySizeTypes(topology.Size, topology.SizeResource) if err != nil { diags.AddError("size parsing error", err.Error()) } if size != nil { topologyElem.Size = size } if topology.ZoneCount.ValueInt64() > 0 { topologyElem.ZoneCount = int32(topology.ZoneCount.ValueInt64()) } if err := topology.parseLegacyNodeType(topologyElem.NodeType); err != nil { diags.AddError("topology legacy node type error", err.Error()) } var nodeRoles []string ds := topology.NodeRoles.ElementsAs(ctx, &nodeRoles, true) diags.Append(ds...) if !ds.HasError() && len(nodeRoles) > 0 { topologyElem.NodeRoles = nodeRoles topologyElem.NodeType = nil } diags.Append(elasticsearchTopologyAutoscalingPayload(ctx, topology.Autoscaling, topologyID, topologyElem)...) diags = append(diags, ds...) return diags } func readElasticsearchTopologies(in *models.ElasticsearchClusterPlan) (ElasticsearchTopologies, error) { if len(in.ClusterTopology) == 0 { return nil, nil } tops := make([]ElasticsearchTopology, 0, len(in.ClusterTopology)) for _, model := range in.ClusterTopology { topology, err := readElasticsearchTopology(model) if err != nil { return nil, err } tops = append(tops, *topology) } return tops, nil } func readElasticsearchTopology(model *models.ElasticsearchClusterTopologyElement) (*ElasticsearchTopology, error) { var topology ElasticsearchTopology topology.id = model.ID if model.InstanceConfigurationID != "" { topology.InstanceConfigurationId = &model.InstanceConfigurationID } if model.InstanceConfigurationVersion != nil { topology.InstanceConfigurationVersion = ec.Int(int(*model.InstanceConfigurationVersion)) } if model.Size != nil { topology.Size = ec.String(util.MemoryToState(*model.Size.Value)) topology.SizeResource = model.Size.Resource } topology.ZoneCount = int(model.ZoneCount) if nt := model.NodeType; nt != nil { if nt.Data != nil { topology.NodeTypeData = ec.String(strconv.FormatBool(*nt.Data)) } if nt.Ingest != nil { topology.NodeTypeIngest = ec.String(strconv.FormatBool(*nt.Ingest)) } if nt.Master != nil { topology.NodeTypeMaster = ec.String(strconv.FormatBool(*nt.Master)) } if nt.Ml != nil { topology.NodeTypeMl = ec.String(strconv.FormatBool(*nt.Ml)) } } topology.NodeRoles = model.NodeRoles autoscaling, err := readElasticsearchTopologyAutoscaling(model) if err != nil { return nil, err } topology.Autoscaling = autoscaling return &topology, nil } func readElasticsearchTopologyAutoscaling(topology *models.ElasticsearchClusterTopologyElement) (*ElasticsearchTopologyAutoscaling, error) { var a ElasticsearchTopologyAutoscaling if max := topology.AutoscalingMax; max != nil { a.MaxSizeResource = max.Resource a.MaxSize = ec.String(util.MemoryToState(*max.Value)) } if min := topology.AutoscalingMin; min != nil { a.MinSizeResource = min.Resource a.MinSize = ec.String(util.MemoryToState(*min.Value)) } if topology.AutoscalingPolicyOverrideJSON != nil { b, err := json.Marshal(topology.AutoscalingPolicyOverrideJSON) if err != nil { return nil, fmt.Errorf("elasticsearch topology %s: unable to persist policy_override_json - %w", topology.ID, err) } a.PolicyOverrideJson = ec.String(string(b)) } if topology.AutoscalingTierOverride != nil { a.TierAutoscale = topology.AutoscalingTierOverride } return &a, nil } func (topology *ElasticsearchTopologyTF) parseLegacyNodeType(nodeType *models.ElasticsearchNodeType) error { if nodeType == nil { return nil } if topology.NodeTypeData.ValueString() != "" { nt, err := strconv.ParseBool(topology.NodeTypeData.ValueString()) if err != nil { return fmt.Errorf("failed parsing node_type_data value: %w", err) } nodeType.Data = &nt } if topology.NodeTypeMaster.ValueString() != "" { nt, err := strconv.ParseBool(topology.NodeTypeMaster.ValueString()) if err != nil { return fmt.Errorf("failed parsing node_type_master value: %w", err) } nodeType.Master = &nt } if topology.NodeTypeIngest.ValueString() != "" { nt, err := strconv.ParseBool(topology.NodeTypeIngest.ValueString()) if err != nil { return fmt.Errorf("failed parsing node_type_ingest value: %w", err) } nodeType.Ingest = &nt } if topology.NodeTypeMl.ValueString() != "" { nt, err := strconv.ParseBool(topology.NodeTypeMl.ValueString()) if err != nil { return fmt.Errorf("failed parsing node_type_ml value: %w", err) } nodeType.Ml = &nt } return nil } func (topology *ElasticsearchTopologyTF) HasNodeTypes() bool { for _, nodeType := range []types.String{topology.NodeTypeData, topology.NodeTypeIngest, topology.NodeTypeMaster, topology.NodeTypeMl} { if !nodeType.IsUnknown() && !nodeType.IsNull() && nodeType.ValueString() != "" { return true } } return false } func (topology *ElasticsearchTopology) HasNodeTypes() bool { if topology != nil { // Check if node types are defined (this means that node roles aren't being used) for _, nodeType := range []*string{topology.NodeTypeData, topology.NodeTypeIngest, topology.NodeTypeMaster, topology.NodeTypeMl} { if nodeType != nil && len(*nodeType) > 0 { return true } } } return false } func (topology *ElasticsearchTopologyTF) checkAvailableMigration(state *ElasticsearchTopologyTF) bool { // 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 !topology.InstanceConfigurationId.IsUnknown() || !topology.InstanceConfigurationVersion.IsUnknown() { return false } instanceConfigIdsDiff := state.InstanceConfigurationId != state.LatestInstanceConfigurationId instanceConfigVersionsDiff := state.InstanceConfigurationVersion != state.LatestInstanceConfigurationVersion // 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 } func objectToTopology(ctx context.Context, obj types.Object) (*ElasticsearchTopologyTF, diag.Diagnostics) { if obj.IsNull() || obj.IsUnknown() { return nil, nil } var topology *ElasticsearchTopologyTF if diags := tfsdk.ValueAs(ctx, obj, &topology); diags.HasError() { return nil, diags } return topology, nil } type ElasticsearchTopologies []ElasticsearchTopology func (es *Elasticsearch) GetTopologies() []*ElasticsearchTopology { topologies := []*ElasticsearchTopology{ es.HotTier, es.WarmTier, es.ColdTier, es.FrozenTier, es.MasterTier, es.CoordinatingTier, es.MlTier, } return topologies } func (tops ElasticsearchTopologies) AsSet() map[string]ElasticsearchTopology { set := make(map[string]ElasticsearchTopology, len(tops)) for _, top := range tops { set[top.id] = top } return set } func matchEsTopologyID(id string, topologies []*models.ElasticsearchClusterTopologyElement) (*models.ElasticsearchClusterTopologyElement, error) { for _, t := range topologies { if t.ID == id { return t, nil } } topIDs := topologyIDs(topologies) for i, id := range topIDs { topIDs[i] = "\"" + id + "\"" } return nil, fmt.Errorf(`invalid id ('%s'): valid topology IDs are %s`, id, strings.Join(topIDs, ", ")) } func topologyIDs(topologies []*models.ElasticsearchClusterTopologyElement) []string { var result []string for _, topology := range topologies { result = append(result, topology.ID) } if len(result) == 0 { return nil } return result } func elasticsearchTopologyAutoscalingPayload(ctx context.Context, autoObj attr.Value, topologyID string, payload *models.ElasticsearchClusterTopologyElement) diag.Diagnostics { var diag diag.Diagnostics if autoObj.IsNull() || autoObj.IsUnknown() { return nil } // it should be only one element if any var autoscale v1.ElasticsearchTopologyAutoscalingTF if diags := tfsdk.ValueAs(ctx, autoObj, &autoscale); diags.HasError() { return diags } if autoscale == (v1.ElasticsearchTopologyAutoscalingTF{}) { return nil } if !autoscale.MinSize.IsNull() && !autoscale.MinSize.IsUnknown() { if payload.AutoscalingMin == nil { payload.AutoscalingMin = new(models.TopologySize) } err := expandAutoscalingDimension(autoscale, payload.AutoscalingMin, autoscale.MinSize, autoscale.MinSizeResource) if err != nil { diag.AddError("fail to parse autoscale min size", err.Error()) return diag } if reflect.DeepEqual(payload.AutoscalingMin, new(models.TopologySize)) { payload.AutoscalingMin = nil } } if !autoscale.MaxSize.IsNull() && !autoscale.MaxSize.IsUnknown() { if payload.AutoscalingMax == nil { payload.AutoscalingMax = new(models.TopologySize) } err := expandAutoscalingDimension(autoscale, payload.AutoscalingMax, autoscale.MaxSize, autoscale.MaxSizeResource) if err != nil { diag.AddError("fail to parse autoscale max size", err.Error()) return diag } if reflect.DeepEqual(payload.AutoscalingMax, new(models.TopologySize)) { payload.AutoscalingMax = nil } } if autoscale.PolicyOverrideJson.ValueString() != "" { if err := json.Unmarshal([]byte(autoscale.PolicyOverrideJson.ValueString()), &payload.AutoscalingPolicyOverrideJSON, ); err != nil { diag.AddError(fmt.Sprintf("elasticsearch topology %s: unable to load policy_override_json", topologyID), err.Error()) return diag } } if !autoscale.TierAutoscale.IsNull() && !autoscale.TierAutoscale.IsUnknown() { payload.AutoscalingTierOverride = autoscale.TierAutoscale.ValueBoolPointer() } return diag } // expandAutoscalingDimension centralises processing of %_size and %_size_resource attributes func expandAutoscalingDimension(autoscale v1.ElasticsearchTopologyAutoscalingTF, model *models.TopologySize, size, sizeResource types.String) error { if size.ValueString() != "" { val, err := deploymentsize.ParseGb(size.ValueString()) if err != nil { return err } model.Value = &val if model.Resource == nil { model.Resource = ec.String("memory") } } if sizeResource.ValueString() != "" { model.Resource = ec.String(sizeResource.ValueString()) } return nil } func SetLatestInstanceConfigInfo(currentTopology *ElasticsearchTopology, latestTopology *models.ElasticsearchClusterTopologyElement) { if currentTopology != nil && latestTopology != nil { currentTopology.LatestInstanceConfigurationId = &latestTopology.InstanceConfigurationID if latestTopology.InstanceConfigurationVersion != nil { currentTopology.LatestInstanceConfigurationVersion = ec.Int(int(*latestTopology.InstanceConfigurationVersion)) } } } func SetLatestInstanceConfigInfoToCurrent(topology *ElasticsearchTopology) { if topology != nil { topology.LatestInstanceConfigurationId = topology.InstanceConfigurationId topology.LatestInstanceConfigurationVersion = topology.InstanceConfigurationVersion } } func GetTopologyFromMigrateRequest(migrateUpdateRequest *deployments.MigrateDeploymentTemplateOK, esTier string) *models.ElasticsearchClusterTopologyElement { var topologyElement *models.ElasticsearchClusterTopologyElement if migrateUpdateRequest.Payload.Resources.Elasticsearch == nil || len(migrateUpdateRequest.Payload.Resources.Elasticsearch) == 0 { return nil } for _, t := range migrateUpdateRequest.Payload.Resources.Elasticsearch[0].Plan.ClusterTopology { if strings.Contains(t.ID, esTier) { topologyElement = t } } return topologyElement }