ec/ecresource/deploymentresource/elasticsearch/v2/elasticsearch_payload.go (277 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"
"slices"
"strings"
"github.com/elastic/cloud-sdk-go/pkg/models"
"github.com/elastic/cloud-sdk-go/pkg/util/ec"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
)
type ElasticsearchTF struct {
Autoscale types.Bool `tfsdk:"autoscale"`
RefId types.String `tfsdk:"ref_id"`
ResourceId types.String `tfsdk:"resource_id"`
Region types.String `tfsdk:"region"`
CloudID types.String `tfsdk:"cloud_id"`
HttpEndpoint types.String `tfsdk:"http_endpoint"`
HttpsEndpoint types.String `tfsdk:"https_endpoint"`
HotContentTier types.Object `tfsdk:"hot"`
CoordinatingTier types.Object `tfsdk:"coordinating"`
MasterTier types.Object `tfsdk:"master"`
WarmTier types.Object `tfsdk:"warm"`
ColdTier types.Object `tfsdk:"cold"`
FrozenTier types.Object `tfsdk:"frozen"`
MlTier types.Object `tfsdk:"ml"`
Config types.Object `tfsdk:"config"`
RemoteCluster types.Set `tfsdk:"remote_cluster"`
Snapshot types.Object `tfsdk:"snapshot"`
SnapshotSource types.Object `tfsdk:"snapshot_source"`
Extension types.Set `tfsdk:"extension"`
TrustAccount types.Set `tfsdk:"trust_account"`
TrustExternal types.Set `tfsdk:"trust_external"`
Strategy types.String `tfsdk:"strategy"`
KeystoreContents types.Map `tfsdk:"keystore_contents"`
}
func ElasticsearchPayload(ctx context.Context, plan types.Object, state *types.Object, updateResources *models.DeploymentUpdateResources, dtID, version string, useNodeRoles bool) (*models.ElasticsearchPayload, diag.Diagnostics) {
es, diags := objectToElasticsearch(ctx, plan)
if diags.HasError() {
return nil, diags
}
if es == nil {
return nil, nil
}
var esState *ElasticsearchTF
if state != nil {
esState, diags = objectToElasticsearch(ctx, *state)
if diags.HasError() {
return nil, diags
}
}
templatePayload := EnrichElasticsearchTemplate(payloadFromUpdate(updateResources), dtID, version, useNodeRoles)
payload, diags := es.payload(ctx, templatePayload, esState)
if diags.HasError() {
return nil, diags
}
return payload, nil
}
func objectToElasticsearch(ctx context.Context, plan types.Object) (*ElasticsearchTF, diag.Diagnostics) {
var es *ElasticsearchTF
if plan.IsNull() || plan.IsUnknown() {
return nil, nil
}
if diags := tfsdk.ValueAs(ctx, plan, &es); diags.HasError() {
return nil, diags
}
return es, nil
}
func CheckAvailableMigration(ctx context.Context, plan types.Object, state types.Object) (bool, diag.Diagnostics) {
esPlan, diags := objectToElasticsearch(ctx, plan)
if diags.HasError() {
return false, diags
}
esState, diags := objectToElasticsearch(ctx, state)
if diags.HasError() {
return false, diags
}
if esPlan == nil || esState == nil {
return false, nil
}
planTiers, diags := esPlan.topologies(ctx)
if diags.HasError() {
return false, diags
}
stateTiers, diags := esState.topologies(ctx)
if diags.HasError() {
return false, diags
}
for topologyId, tier := range planTiers {
if tier != nil && stateTiers[topologyId] != nil && tier.checkAvailableMigration(stateTiers[topologyId]) {
return true, nil
}
}
return false, nil
}
func (es *ElasticsearchTF) payload(ctx context.Context, res *models.ElasticsearchPayload, state *ElasticsearchTF) (*models.ElasticsearchPayload, diag.Diagnostics) {
var diags diag.Diagnostics
if !es.RefId.IsNull() {
res.RefID = ec.String(es.RefId.ValueString())
}
if es.Region.ValueString() != "" {
res.Region = ec.String(es.Region.ValueString())
}
// Unsetting the curation properties is since they're deprecated since
// >= 6.6.0 which is when ILM is introduced in Elasticsearch.
unsetElasticsearchCuration(res)
var ds diag.Diagnostics
diags.Append(es.topologiesPayload(ctx, res.Plan.ClusterTopology)...)
// Fixes the node_roles field to remove the dedicated tier roles from the
// list when these are set as a dedicated tier as a topology element.
updateNodeRolesOnDedicatedTiers(res.Plan.ClusterTopology)
res.Plan.Elasticsearch, ds = elasticsearchConfigPayload(ctx, es.Config, res.Plan.Elasticsearch)
diags.Append(ds...)
res.Settings, ds = elasticsearchSnapshotPayload(ctx, es.Snapshot, res.Settings, state)
diags.Append(ds...)
diags.Append(elasticsearchSnapshotSourcePayload(ctx, es.SnapshotSource, res.Plan)...)
diags.Append(elasticsearchExtensionPayload(ctx, es.Extension, res.Plan.Elasticsearch)...)
if !es.Autoscale.IsNull() && !es.Autoscale.IsUnknown() {
res.Plan.AutoscalingEnabled = ec.Bool(es.Autoscale.ValueBool())
}
// Only add trust settings to update payload if trust has changed
if state == nil || !es.TrustAccount.Equal(state.TrustAccount) || !es.TrustExternal.Equal(state.TrustExternal) {
res.Settings, ds = elasticsearchTrustAccountPayload(ctx, es.TrustAccount, res.Settings)
diags.Append(ds...)
res.Settings, ds = elasticsearchTrustExternalPayload(ctx, es.TrustExternal, res.Settings)
diags.Append(ds...)
}
res.Settings, ds = elasticsearchKeystoreContentsPayload(ctx, es.KeystoreContents, res.Settings, state)
diags.Append(ds...)
elasticsearchStrategyPayload(es.Strategy, res.Plan)
return res, diags
}
func (es *ElasticsearchTF) topologyObjects() map[string]types.Object {
return map[string]types.Object{
"hot_content": es.HotContentTier,
"warm": es.WarmTier,
"cold": es.ColdTier,
"frozen": es.FrozenTier,
"ml": es.MlTier,
"master": es.MasterTier,
"coordinating": es.CoordinatingTier,
}
}
func (es *ElasticsearchTF) topologies(ctx context.Context) (map[string]*ElasticsearchTopologyTF, diag.Diagnostics) {
var diagnostics diag.Diagnostics
tierObjects := es.topologyObjects()
res := make(map[string]*ElasticsearchTopologyTF, len(tierObjects))
for topologyId, topologyObject := range tierObjects {
tier, diags := objectToTopology(ctx, topologyObject)
diagnostics.Append(diags...)
res[topologyId] = tier
}
return res, diagnostics
}
func (es *ElasticsearchTF) topologiesPayload(ctx context.Context, topologyModels []*models.ElasticsearchClusterTopologyElement) diag.Diagnostics {
tiers, diags := es.topologies(ctx)
if diags.HasError() {
return diags
}
for tierId, tier := range tiers {
if tier != nil {
diags.Append(tier.payload(ctx, tierId, topologyModels)...)
}
}
return diags
}
func unsetElasticsearchCuration(payload *models.ElasticsearchPayload) {
if payload.Plan.Elasticsearch != nil {
payload.Plan.Elasticsearch.Curation = nil
}
if payload.Settings != nil {
payload.Settings.Curation = nil
}
}
func updateNodeRolesOnDedicatedTiers(topologies []*models.ElasticsearchClusterTopologyElement) {
dataTier, hasMasterTier, hasIngestTier := dedicatedTopoogies(topologies)
// This case is not very likely since all deployments will have a data tier.
// It's here because the code path is technically possible and it's better
// than a straight panic.
if dataTier == nil {
return
}
if hasIngestTier {
dataTier.NodeRoles = removeItemFromSlice(
dataTier.NodeRoles, ingestDataTierRole,
)
}
if hasMasterTier {
dataTier.NodeRoles = removeItemFromSlice(
dataTier.NodeRoles, masterDataTierRole,
)
}
}
func removeItemFromSlice(slice []string, item string) []string {
i := slices.Index(slice, item)
if i == -1 {
return slice
}
return slices.Delete(slice, i, i+1)
}
func dedicatedTopoogies(topologies []*models.ElasticsearchClusterTopologyElement) (dataTier *models.ElasticsearchClusterTopologyElement, hasMasterTier, hasIngestTier bool) {
for _, topology := range topologies {
var hasSomeDataRole bool
var hasMasterRole bool
var hasIngestRole bool
for _, role := range topology.NodeRoles {
sizeNonZero := *topology.Size.Value > 0
if strings.HasPrefix(role, dataTierRolePrefix) && sizeNonZero {
hasSomeDataRole = true
}
if role == ingestDataTierRole && sizeNonZero {
hasIngestRole = true
}
if role == masterDataTierRole && sizeNonZero {
hasMasterRole = true
}
}
if !hasSomeDataRole && hasMasterRole {
hasMasterTier = true
}
if !hasSomeDataRole && hasIngestRole {
hasIngestTier = true
}
if hasSomeDataRole && hasMasterRole {
dataTier = topology
}
}
return dataTier, hasMasterTier, hasIngestTier
}
func elasticsearchStrategyPayload(strategy types.String, payload *models.ElasticsearchClusterPlan) {
createModelIfNeeded := func() {
if payload.Transient == nil {
payload.Transient = &models.TransientElasticsearchPlanConfiguration{}
}
if payload.Transient.Strategy == nil {
payload.Transient.Strategy = &models.PlanStrategy{}
}
}
switch strategy.ValueString() {
case strategyAutodetect:
createModelIfNeeded()
payload.Transient.Strategy.Autodetect = models.AutodetectStrategyConfig(map[string]interface{}{})
case strategyGrowAndShrink:
createModelIfNeeded()
payload.Transient.Strategy.GrowAndShrink = models.GrowShrinkStrategyConfig(map[string]interface{}{})
case strategyRollingGrowAndShrink:
createModelIfNeeded()
payload.Transient.Strategy.RollingGrowAndShrink = models.RollingGrowShrinkStrategyConfig(map[string]interface{}{})
case strategyRollingAll:
createModelIfNeeded()
payload.Transient.Strategy.Rolling = &models.RollingStrategyConfig{
GroupBy: "__all__",
}
}
}
func payloadFromUpdate(updateResources *models.DeploymentUpdateResources) *models.ElasticsearchPayload {
if updateResources == nil || len(updateResources.Elasticsearch) == 0 {
return &models.ElasticsearchPayload{
Plan: &models.ElasticsearchClusterPlan{
Elasticsearch: &models.ElasticsearchConfiguration{},
},
Settings: &models.ElasticsearchClusterSettings{},
}
}
return updateResources.Elasticsearch[0]
}
func EnrichElasticsearchTemplate(tpl *models.ElasticsearchPayload, templateId, version string, useNodeRoles bool) *models.ElasticsearchPayload {
if tpl.Plan.DeploymentTemplate == nil {
tpl.Plan.DeploymentTemplate = &models.DeploymentTemplateReference{}
}
if tpl.Plan.DeploymentTemplate.ID == nil || *tpl.Plan.DeploymentTemplate.ID == "" {
tpl.Plan.DeploymentTemplate.ID = ec.String(templateId)
}
if tpl.Plan.Elasticsearch.Version == "" {
tpl.Plan.Elasticsearch.Version = version
}
for _, topology := range tpl.Plan.ClusterTopology {
if useNodeRoles {
topology.NodeType = nil
continue
}
topology.NodeRoles = nil
}
return tpl
}