Skip to content

Commit

Permalink
Utilise the migrate template API when applying changes to the deploym…
Browse files Browse the repository at this point in the history
…ent template id (#547)

* Utilise the migrate template API when applying changes to the deployment template id

* Add changelog

* Fix acceptance tests

* Reset the instance_configuration_id so changes from the migration API are picked up
  • Loading branch information
tobio authored Nov 17, 2022
1 parent 3807263 commit e6a5f34
Show file tree
Hide file tree
Showing 12 changed files with 382 additions and 113 deletions.
3 changes: 3 additions & 0 deletions .changelog/547.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
resource/deployment: Utilise the template migration API to build the base update request when changing `deployment_template_id`. This results in more reliable changes between deployment templates.
```
96 changes: 96 additions & 0 deletions ec/acc/deployment_template_migration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// 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 acc

import (
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccDeployment_template_migration(t *testing.T) {
resName := "ec_deployment.compute_optimized"
randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
basicCfg := "testdata/deployment_compute_optimized_1.tf"
region := getRegion()
cfg := fixtureAccDeploymentResourceBasicDefaults(t, basicCfg, randomName, region, computeOpTemplate)
secondConfigCfg := fixtureAccDeploymentResourceBasicDefaults(t, basicCfg, randomName, region, memoryOpTemplate)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactory,
CheckDestroy: testAccDeploymentDestroy,
Steps: []resource.TestStep{
{
// Create a Compute Optimized deployment with the default settings.
Config: cfg,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(resName, "deployment_template_id", setDefaultTemplate(region, computeOpTemplate)),
resource.TestCheckResourceAttr(resName, "elasticsearch.#", "1"),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.#", "1"),
resource.TestCheckResourceAttrSet(resName, "elasticsearch.0.topology.0.instance_configuration_id"),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.size", "8g"),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.size_resource", "memory"),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.node_type_data", ""),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.node_type_ingest", ""),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.node_type_master", ""),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.node_type_ml", ""),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.id", "hot_content"),
resource.TestCheckResourceAttrSet(resName, "elasticsearch.0.topology.0.node_roles.#"),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.zone_count", "2"),
resource.TestCheckResourceAttr(resName, "kibana.#", "1"),
resource.TestCheckResourceAttr(resName, "kibana.0.topology.0.zone_count", "1"),
resource.TestCheckResourceAttr(resName, "kibana.0.topology.#", "1"),
resource.TestCheckResourceAttrSet(resName, "kibana.0.topology.0.instance_configuration_id"),
resource.TestCheckResourceAttr(resName, "kibana.0.topology.0.size", "1g"),
resource.TestCheckResourceAttr(resName, "kibana.0.topology.0.size_resource", "memory"),
resource.TestCheckResourceAttr(resName, "apm.#", "0"),
resource.TestCheckResourceAttr(resName, "enterprise_search.#", "0"),
),
},
{
// Change the deployment to memory optimized
Config: secondConfigCfg,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(resName, "deployment_template_id", setDefaultTemplate(region, memoryOpTemplate)),
resource.TestCheckResourceAttr(resName, "elasticsearch.#", "1"),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.#", "1"),
resource.TestCheckResourceAttrSet(resName, "elasticsearch.0.topology.0.instance_configuration_id"),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.size", "8g"),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.size_resource", "memory"),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.node_type_data", ""),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.node_type_ingest", ""),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.node_type_master", ""),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.node_type_ml", ""),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.id", "hot_content"),
resource.TestCheckResourceAttrSet(resName, "elasticsearch.0.topology.0.node_roles.#"),
resource.TestCheckResourceAttr(resName, "elasticsearch.0.topology.0.zone_count", "2"),
resource.TestCheckResourceAttr(resName, "kibana.#", "1"),
resource.TestCheckResourceAttr(resName, "kibana.0.topology.0.zone_count", "1"),
resource.TestCheckResourceAttr(resName, "kibana.0.topology.#", "1"),
resource.TestCheckResourceAttrSet(resName, "kibana.0.topology.0.instance_configuration_id"),
resource.TestCheckResourceAttr(resName, "kibana.0.topology.0.size", "1g"),
resource.TestCheckResourceAttr(resName, "kibana.0.topology.0.size_resource", "memory"),
resource.TestCheckResourceAttr(resName, "apm.#", "0"),
resource.TestCheckResourceAttr(resName, "enterprise_search.#", "0"),
),
},
},
})
}
13 changes: 11 additions & 2 deletions ec/acc/testdata/deployment_basic_defaults_hw_2.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ resource "ec_deployment" "defaults" {
version = data.ec_stack.latest.version
deployment_template_id = "%s"

elasticsearch {}
elasticsearch {
topology {
id = "hot_content"
size = "4g"
}
topology {
id = "warm"
size = "4g"
}
}

kibana {}
}
}
9 changes: 9 additions & 0 deletions ec/ecresource/deploymentresource/apm_expanders.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,12 @@ func apmResource(res *models.DeploymentTemplateInfoV2) *models.ApmPayload {
}
return res.DeploymentTemplate.Resources.Apm[0]
}

// apmResourceFromUpdate returns the ApmPayload from a deployment
// update request or an empty version of the payload.
func apmResourceFromUpdate(res *models.DeploymentUpdateResources) *models.ApmPayload {
if len(res.Apm) == 0 {
return nil
}
return res.Apm[0]
}
26 changes: 20 additions & 6 deletions ec/ecresource/deploymentresource/elasticsearch_expanders.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,20 +374,34 @@ func matchEsTopologyID(id string, topologies []*models.ElasticsearchClusterTopol
)
}

func emptyEsResource() *models.ElasticsearchPayload {
return &models.ElasticsearchPayload{
Plan: &models.ElasticsearchClusterPlan{
Elasticsearch: &models.ElasticsearchConfiguration{},
},
Settings: &models.ElasticsearchClusterSettings{},
}
}

// esResource returns the ElaticsearchPayload from a deployment
// template or an empty version of the payload.
func esResource(res *models.DeploymentTemplateInfoV2) *models.ElasticsearchPayload {
if len(res.DeploymentTemplate.Resources.Elasticsearch) == 0 {
return &models.ElasticsearchPayload{
Plan: &models.ElasticsearchClusterPlan{
Elasticsearch: &models.ElasticsearchConfiguration{},
},
Settings: &models.ElasticsearchClusterSettings{},
}
return emptyEsResource()
}
return res.DeploymentTemplate.Resources.Elasticsearch[0]
}

// esResourceFromUpdate returns the ElaticsearchPayload from a deployment
// update request or an empty version of the payload.
func esResourceFromUpdate(res *models.DeploymentUpdateResources) *models.ElasticsearchPayload {
if len(res.Elasticsearch) == 0 {
return emptyEsResource()
}

return res.Elasticsearch[0]
}

func unsetElasticsearchCuration(payload *models.ElasticsearchPayload) {
if payload.Plan.Elasticsearch != nil {
payload.Plan.Elasticsearch.Curation = nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,12 @@ func essResource(res *models.DeploymentTemplateInfoV2) *models.EnterpriseSearchP
}
return res.DeploymentTemplate.Resources.EnterpriseSearch[0]
}

// essResourceFromUpdate returns the EnterpriseSearchPayload from a deployment
// update request or an empty version of the payload.
func essResourceFromUpdate(res *models.DeploymentUpdateResources) *models.EnterpriseSearchPayload {
if len(res.EnterpriseSearch) == 0 {
return nil
}
return res.EnterpriseSearch[0]
}
118 changes: 88 additions & 30 deletions ec/ecresource/deploymentresource/expanders.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
semver "github.com/blang/semver/v4"
"github.com/elastic/cloud-sdk-go/pkg/api"
"github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/deptemplateapi"
"github.com/elastic/cloud-sdk-go/pkg/client/deployments"
"github.com/elastic/cloud-sdk-go/pkg/models"
"github.com/elastic/cloud-sdk-go/pkg/multierror"
"github.com/elastic/cloud-sdk-go/pkg/util/ec"
Expand Down Expand Up @@ -123,6 +124,70 @@ func createResourceToModel(d *schema.ResourceData, client *api.API) (*models.Dep
return &result, nil
}

func getBaseUpdatePayloads(d *schema.ResourceData, client *api.API) (*models.DeploymentUpdateResources, error) {
prevDtId, dtIdIf := d.GetChange("deployment_template_id")
dtId := dtIdIf.(string)
template, err := deptemplateapi.Get(deptemplateapi.GetParams{
API: client,
TemplateID: dtId,
Region: d.Get("region").(string),
HideInstanceConfigurations: true,
})

if err != nil {
return nil, err
}

baseUpdatePayloads := &models.DeploymentUpdateResources{
Apm: template.DeploymentTemplate.Resources.Apm,
Appsearch: template.DeploymentTemplate.Resources.Appsearch,
Elasticsearch: template.DeploymentTemplate.Resources.Elasticsearch,
EnterpriseSearch: template.DeploymentTemplate.Resources.EnterpriseSearch,
IntegrationsServer: template.DeploymentTemplate.Resources.IntegrationsServer,
Kibana: template.DeploymentTemplate.Resources.Kibana,
}

// If the deployment template has changed then we should use the template migration API
// to build the base update payloads
if d.HasChange("deployment_template_id") && prevDtId.(string) != "" {
// Get an update request from the template migration API
migrateUpdateRequest, err := client.V1API.Deployments.MigrateDeploymentTemplate(
deployments.NewMigrateDeploymentTemplateParams().WithDeploymentID(d.Id()).WithTemplateID(dtId),
client.AuthWriter,
)

if err != nil {
return nil, err
}

if len(migrateUpdateRequest.Payload.Resources.Apm) > 0 {
baseUpdatePayloads.Apm = migrateUpdateRequest.Payload.Resources.Apm
}

if len(migrateUpdateRequest.Payload.Resources.Appsearch) > 0 {
baseUpdatePayloads.Appsearch = migrateUpdateRequest.Payload.Resources.Appsearch
}

if len(migrateUpdateRequest.Payload.Resources.Elasticsearch) > 0 {
baseUpdatePayloads.Elasticsearch = migrateUpdateRequest.Payload.Resources.Elasticsearch
}

if len(migrateUpdateRequest.Payload.Resources.EnterpriseSearch) > 0 {
baseUpdatePayloads.EnterpriseSearch = migrateUpdateRequest.Payload.Resources.EnterpriseSearch
}

if len(migrateUpdateRequest.Payload.Resources.IntegrationsServer) > 0 {
baseUpdatePayloads.IntegrationsServer = migrateUpdateRequest.Payload.Resources.IntegrationsServer
}

if len(migrateUpdateRequest.Payload.Resources.Kibana) > 0 {
baseUpdatePayloads.Kibana = migrateUpdateRequest.Payload.Resources.Kibana
}
}

return baseUpdatePayloads, nil
}

func updateResourceToModel(d *schema.ResourceData, client *api.API) (*models.DeploymentUpdateRequest, error) {
var result = models.DeploymentUpdateRequest{
Name: d.Get("name").(string),
Expand All @@ -133,36 +198,24 @@ func updateResourceToModel(d *schema.ResourceData, client *api.API) (*models.Dep
Metadata: &models.DeploymentUpdateMetadata{},
}

dtID := d.Get("deployment_template_id").(string)
version := d.Get("version").(string)
template, err := deptemplateapi.Get(deptemplateapi.GetParams{
API: client,
TemplateID: dtID,
Region: d.Get("region").(string),
HideInstanceConfigurations: true,
})
updatePayloads, err := getBaseUpdatePayloads(d, client)
if err != nil {
return nil, err
}

dtID := d.Get("deployment_template_id").(string)
version := d.Get("version").(string)
es := d.Get("elasticsearch").([]interface{})
kibana := d.Get("kibana").([]interface{})
apm := d.Get("apm").([]interface{})
integrationsServer := d.Get("integrations_server").([]interface{})
enterpriseSearch := d.Get("enterprise_search").([]interface{})

// When the deployment template is changed, we need to unset the missing
// resource topologies to account for a new instance_configuration_id and
// a different default value.
prevDT, _ := d.GetChange("deployment_template_id")
if d.HasChange("deployment_template_id") && prevDT.(string) != "" {
// If the deployment_template_id is changed, then we unset the
// Elasticsearch topology to account for the case where the
// instance_configuration_id changes, i.e. Hot / Warm, etc.

// This might not be necessary going forward as we move to
// tiered Elasticsearch nodes.
unsetTopology(es)
if d.HasChange("deployment_template_id") && prevDT != "" {
unsetInstanceConfigurations([][]interface{}{
es, kibana, apm, integrationsServer, enterpriseSearch,
})
}

useNodeRoles, err := compatibleWithNodeRoles(version)
Expand All @@ -178,7 +231,7 @@ func updateResourceToModel(d *schema.ResourceData, client *api.API) (*models.Dep
merr := multierror.NewPrefixed("invalid configuration")
esRes, err := expandEsResources(
es, enrichElasticsearchTemplate(
esResource(template), dtID, version, useNodeRoles,
esResourceFromUpdate(updatePayloads), dtID, version, useNodeRoles,
),
)
if err != nil {
Expand All @@ -191,25 +244,25 @@ func updateResourceToModel(d *schema.ResourceData, client *api.API) (*models.Dep
// to "partial".
ensurePartialSnapshotStrategy(esRes)

kibanaRes, err := expandKibanaResources(kibana, kibanaResource(template))
kibanaRes, err := expandKibanaResources(kibana, kibanaResourceFromUpdate(updatePayloads))
if err != nil {
merr = merr.Append(err)
}
result.Resources.Kibana = append(result.Resources.Kibana, kibanaRes...)

apmRes, err := expandApmResources(apm, apmResource(template))
apmRes, err := expandApmResources(apm, apmResourceFromUpdate(updatePayloads))
if err != nil {
merr = merr.Append(err)
}
result.Resources.Apm = append(result.Resources.Apm, apmRes...)

integrationsServerRes, err := expandIntegrationsServerResources(integrationsServer, integrationsServerResource(template))
integrationsServerRes, err := expandIntegrationsServerResources(integrationsServer, integrationsServerResourceFromUpdate(updatePayloads))
if err != nil {
merr = merr.Append(err)
}
result.Resources.IntegrationsServer = append(result.Resources.IntegrationsServer, integrationsServerRes...)

enterpriseSearchRes, err := expandEssResources(enterpriseSearch, essResource(template))
enterpriseSearchRes, err := expandEssResources(enterpriseSearch, essResourceFromUpdate(updatePayloads))
if err != nil {
merr = merr.Append(err)
}
Expand Down Expand Up @@ -237,6 +290,17 @@ func updateResourceToModel(d *schema.ResourceData, client *api.API) (*models.Dep
return &result, nil
}

func unsetInstanceConfigurations(rawResources [][]interface{}) {
for _, resource := range rawResources {
for _, r := range resource {
topologies := r.(map[string]interface{})["topology"].([]interface{})
for _, topology := range topologies {
delete(topology.(map[string]interface{}), "instance_configuration_id")
}
}
}
}

func enrichElasticsearchTemplate(tpl *models.ElasticsearchPayload, dt, version string, useNodeRoles bool) *models.ElasticsearchPayload {
if tpl.Plan.DeploymentTemplate == nil {
tpl.Plan.DeploymentTemplate = &models.DeploymentTemplateReference{}
Expand All @@ -261,12 +325,6 @@ func enrichElasticsearchTemplate(tpl *models.ElasticsearchPayload, dt, version s
return tpl
}

func unsetTopology(rawRes []interface{}) {
for _, r := range rawRes {
delete(r.(map[string]interface{}), "topology")
}
}

func expandTags(raw map[string]interface{}) []*models.MetadataItem {
result := make([]*models.MetadataItem, 0, len(raw))
for k, v := range raw {
Expand Down
Loading

0 comments on commit e6a5f34

Please sign in to comment.