ec/ecresource/deploymentresource/update.go (173 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"
"github.com/elastic/cloud-sdk-go/pkg/api"
"github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi"
"github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/depresourceapi"
"github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/trafficfilterapi"
v2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/deployment/v2"
"github.com/elastic/terraform-provider-ec/ec/internal/util"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
)
func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan v2.DeploymentTF
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
var state v2.DeploymentTF
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
// Read migrate request from private state
migrateTemplateRequest, diags := ReadPrivateStateMigrateTemplateRequest(ctx, req.Private)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}
updateReq, diags := plan.UpdateRequest(ctx, r.client, state, migrateTemplateRequest)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}
res, err := deploymentapi.Update(deploymentapi.UpdateParams{
API: r.client,
DeploymentID: plan.Id.ValueString(),
Request: updateReq,
Overrides: deploymentapi.PayloadOverrides{
Version: plan.Version.ValueString(),
Region: plan.Region.ValueString(),
},
})
if err != nil {
resp.Diagnostics.AddError("failed updating deployment", err.Error())
return
}
if err := WaitForPlanCompletion(r.client, plan.Id.ValueString()); err != nil {
resp.Diagnostics.AddError("failed tracking update progress", err.Error())
return
}
privateFilters, d := readPrivateStateTrafficFilters(ctx, req.Private)
resp.Diagnostics.Append(d...)
if resp.Diagnostics.HasError() {
return
}
planRules, diags := HandleTrafficFilterChange(ctx, r.client, plan, privateFilters)
resp.Diagnostics.Append(diags...)
updatePrivateStateTrafficFilters(ctx, resp.Private, planRules)
resp.Diagnostics.Append(v2.HandleRemoteClusters(ctx, r.client, plan.Id.ValueString(), plan.Elasticsearch)...)
deployment, diags := r.read(ctx, plan.Id.ValueString(), &state, &plan, res.Resources, planRules, nil)
resp.Diagnostics.Append(diags...)
if deployment == nil {
resp.Diagnostics.AddError("cannot read just updated resource", "")
resp.State.RemoveResource(ctx)
return
}
if plan.ResetElasticsearchPassword.ValueBool() {
newUsername, newPassword, diags := r.ResetElasticsearchPassword(plan.Id.ValueString(), *deployment.Elasticsearch.RefId)
if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() {
return
}
deployment.ElasticsearchUsername = newUsername
deployment.ElasticsearchPassword = newPassword
}
resp.Diagnostics.Append(resp.State.Set(ctx, deployment)...)
}
func (r *Resource) ResetElasticsearchPassword(deploymentID string, refID string) (string, string, diag.Diagnostics) {
var diags diag.Diagnostics
resetResp, err := depresourceapi.ResetElasticsearchPassword(depresourceapi.ResetElasticsearchPasswordParams{
API: r.client,
ID: deploymentID,
RefID: refID,
})
if err != nil {
diags.AddError("failed to reset elasticsearch password", err.Error())
return "", "", diags
}
return *resetResp.Username, *resetResp.Password, diags
}
func HandleTrafficFilterChange(ctx context.Context, client *api.API, plan v2.DeploymentTF, stateRules ruleSet) ([]string, diag.Diagnostics) {
var planRules ruleSet
if diags := plan.TrafficFilter.ElementsAs(ctx, &planRules, true); diags.HasError() {
return []string{}, diags
}
var rulesToAdd, rulesToDelete []string
for _, rule := range planRules {
if !stateRules.exist(rule) {
rulesToAdd = append(rulesToAdd, rule)
}
}
for _, rule := range stateRules {
if !planRules.exist(rule) {
rulesToDelete = append(rulesToDelete, rule)
}
}
var diags diag.Diagnostics
for _, rule := range rulesToAdd {
if err := associateRule(rule, plan.Id.ValueString(), client); err != nil {
diags.AddError("cannot associate traffic filter rule", err.Error())
}
}
for _, rule := range rulesToDelete {
if err := removeRule(rule, plan.Id.ValueString(), client); err != nil {
diags.AddError("cannot remove traffic filter rule", err.Error())
}
}
return planRules, diags
}
type ruleSet []string
func (rs ruleSet) exist(rule string) bool {
for _, r := range rs {
if r == rule {
return true
}
}
return false
}
var (
GetAssociation = trafficfilterapi.Get
CreateAssociation = trafficfilterapi.CreateAssociation
DeleteAssociation = trafficfilterapi.DeleteAssociation
)
func associateRule(ruleID, deploymentID string, client *api.API) error {
res, err := GetAssociation(trafficfilterapi.GetParams{
API: client, ID: ruleID, IncludeAssociations: true,
})
if err != nil {
return err
}
// When the rule has already been associated, return.
for _, assoc := range res.Associations {
if deploymentID == *assoc.ID {
return nil
}
}
// Create assignment.
if err := CreateAssociation(trafficfilterapi.CreateAssociationParams{
API: client, ID: ruleID, EntityType: "deployment", EntityID: deploymentID,
}); err != nil {
return err
}
return nil
}
func removeRule(ruleID, deploymentID string, client *api.API) error {
res, err := GetAssociation(trafficfilterapi.GetParams{
API: client, ID: ruleID, IncludeAssociations: true,
})
// If the rule is gone (403 or 404), return nil.
if err != nil {
if util.TrafficFilterNotFound(err) {
return nil
}
return err
}
// If the rule is found, then delete the association.
for _, assoc := range res.Associations {
if deploymentID == *assoc.ID {
return DeleteAssociation(trafficfilterapi.DeleteAssociationParams{
API: client,
ID: ruleID,
EntityID: *assoc.ID,
EntityType: *assoc.EntityType,
})
}
}
return nil
}