diff --git a/ec/ecresource/deploymentresource/create.go b/ec/ecresource/deploymentresource/create.go index 138bc36fa..ef2e5f304 100644 --- a/ec/ecresource/deploymentresource/create.go +++ b/ec/ecresource/deploymentresource/create.go @@ -81,7 +81,13 @@ func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp resp.Diagnostics.Append(v2.HandleRemoteClusters(ctx, r.client, *res.ID, plan.Elasticsearch)...) - deployment, diags := r.read(ctx, *res.ID, nil, &plan, res.Resources) + filters := []string{} + if request.Settings != nil && request.Settings.TrafficFilterSettings != nil && request.Settings.TrafficFilterSettings.Rulesets != nil { + filters = request.Settings.TrafficFilterSettings.Rulesets + } + + deployment, diags := r.read(ctx, *res.ID, nil, &plan, res.Resources, filters) + updatePrivateStateTrafficFiltersFromCreate(ctx, resp, filters) resp.Diagnostics.Append(diags...) diff --git a/ec/ecresource/deploymentresource/deployment/v2/deployment_read.go b/ec/ecresource/deploymentresource/deployment/v2/deployment_read.go index 4bd277fb2..e31254c8a 100644 --- a/ec/ecresource/deploymentresource/deployment/v2/deployment_read.go +++ b/ec/ecresource/deploymentresource/deployment/v2/deployment_read.go @@ -230,14 +230,19 @@ func (dep *Deployment) ProcessSelfInObservability() { } } -func (dep *Deployment) HandleEmptyTrafficFilters(ctx context.Context, base DeploymentTF) diag.Diagnostics { +func (dep *Deployment) HandleEmptyTrafficFilters(ctx context.Context, base DeploymentTF, privateFilters []string) diag.Diagnostics { var baseFilters []string diags := base.TrafficFilter.ElementsAs(ctx, &baseFilters, true) if diags.HasError() { return diags } - // Only include traffic filters which are part of the TF plan. + for _, filter := range privateFilters { + if !slices.Contains[string](baseFilters, filter) { + baseFilters = append(baseFilters, filter) + } + } + if len(baseFilters) == 0 { dep.TrafficFilter = baseFilters } @@ -251,6 +256,21 @@ func (dep *Deployment) HandleEmptyTrafficFilters(ctx context.Context, base Deplo dep.TrafficFilter = intersectionFilters + // // Ensure consistency between null, and empty configured traffic filter values. + // // The Cloud API represents an empty set of traffic filters as a null/missing value. Terraform does distinguish between those two cases. + // // If the Cloud response does not include traffic filters, then set the read value as the planned value, but only if the planned value is empty. + // if dep.TrafficFilter == nil { + // var baseFilters []string + // diags := base.TrafficFilter.ElementsAs(ctx, &baseFilters, true) + // if diags.HasError() { + // return diags + // } + + // if len(baseFilters) == 0 { + // dep.TrafficFilter = baseFilters + // } + // } + return diags } diff --git a/ec/ecresource/deploymentresource/private_state.go b/ec/ecresource/deploymentresource/private_state.go new file mode 100644 index 000000000..b036e11c2 --- /dev/null +++ b/ec/ecresource/deploymentresource/private_state.go @@ -0,0 +1,73 @@ +// 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" + "encoding/json" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +const trafficFilterStateKey = "traffic_filters" + +func readPrivateStateTrafficFiltersFromRead(ctx context.Context, req resource.ReadRequest) ([]string, diag.Diagnostics) { + return readPrivateStateTrafficFilters(req.Private.GetKey(ctx, trafficFilterStateKey)) +} + +func readPrivateStateTrafficFiltersFromUpdate(ctx context.Context, req resource.UpdateRequest) ([]string, diag.Diagnostics) { + return readPrivateStateTrafficFilters(req.Private.GetKey(ctx, trafficFilterStateKey)) +} + +func readPrivateStateTrafficFilters(privateFilterBytes []byte, diags diag.Diagnostics) ([]string, diag.Diagnostics) { + if privateFilterBytes == nil || diags.HasError() { + return []string{}, diags + } + + var privateFilters []string + err := json.Unmarshal(privateFilterBytes, &privateFilters) + if err != nil { + diags.AddError("failed to parse private state", err.Error()) + return []string{}, diags + } + + return privateFilters, diags +} + +func updatePrivateStateTrafficFiltersFromUpdate(ctx context.Context, resp *resource.UpdateResponse, filters []string) diag.Diagnostics { + var diags diag.Diagnostics + filterBytes, err := json.Marshal(filters) + if err != nil { + diags.AddError("failed to update private state", err.Error()) + return diags + } + + return resp.Private.SetKey(ctx, trafficFilterStateKey, filterBytes) +} + +func updatePrivateStateTrafficFiltersFromCreate(ctx context.Context, resp *resource.CreateResponse, filters []string) diag.Diagnostics { + var diags diag.Diagnostics + filterBytes, err := json.Marshal(filters) + if err != nil { + diags.AddError("failed to update private state", err.Error()) + return diags + } + + return resp.Private.SetKey(ctx, trafficFilterStateKey, filterBytes) +} diff --git a/ec/ecresource/deploymentresource/read.go b/ec/ecresource/deploymentresource/read.go index b3cf1e135..7a2412164 100644 --- a/ec/ecresource/deploymentresource/read.go +++ b/ec/ecresource/deploymentresource/read.go @@ -57,8 +57,14 @@ func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, respo var newState *deploymentv2.Deployment + privateFilters, d := readPrivateStateTrafficFiltersFromRead(ctx, request) + response.Diagnostics.Append(d...) + if response.Diagnostics.HasError() { + return + } + // use state for the plan (there is no plan and config during Read) - otherwise we can get unempty plan output - newState, diags = r.read(ctx, curState.Id.ValueString(), &curState, nil, nil) + newState, diags = r.read(ctx, curState.Id.ValueString(), &curState, nil, nil, privateFilters) response.Diagnostics.Append(diags...) @@ -74,7 +80,7 @@ func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, respo } // at least one of state and plan should not be nil -func (r *Resource) read(ctx context.Context, id string, state *deploymentv2.DeploymentTF, plan *deploymentv2.DeploymentTF, deploymentResources []*models.DeploymentResource) (*deploymentv2.Deployment, diag.Diagnostics) { +func (r *Resource) read(ctx context.Context, id string, state *deploymentv2.DeploymentTF, plan *deploymentv2.DeploymentTF, deploymentResources []*models.DeploymentResource, privateFilters []string) (*deploymentv2.Deployment, diag.Diagnostics) { var diags diag.Diagnostics var base deploymentv2.DeploymentTF @@ -158,7 +164,7 @@ func (r *Resource) read(ctx context.Context, id string, state *deploymentv2.Depl deployment.ResetElasticsearchPassword = base.ResetElasticsearchPassword.ValueBoolPointer() } - diags.Append(deployment.HandleEmptyTrafficFilters(ctx, base)...) + diags.Append(deployment.HandleEmptyTrafficFilters(ctx, base, privateFilters)...) deployment.SetCredentialsIfEmpty(state) diff --git a/ec/ecresource/deploymentresource/update.go b/ec/ecresource/deploymentresource/update.go index b6e77a9fb..a1c23f90c 100644 --- a/ec/ecresource/deploymentresource/update.go +++ b/ec/ecresource/deploymentresource/update.go @@ -73,11 +73,17 @@ func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp return } - resp.Diagnostics.Append(HandleTrafficFilterChange(ctx, r.client, plan, state)...) - + privateFilters, d := readPrivateStateTrafficFiltersFromUpdate(ctx, req) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + planRules, diags := HandleTrafficFilterChange(ctx, r.client, plan, privateFilters) + resp.Diagnostics.Append(diags...) + updatePrivateStateTrafficFiltersFromUpdate(ctx, resp, 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) + deployment, diags := r.read(ctx, plan.Id.ValueString(), &state, &plan, res.Resources, planRules) resp.Diagnostics.Append(diags...) @@ -116,18 +122,14 @@ func (r *Resource) ResetElasticsearchPassword(deploymentID string, refID string) return *resetResp.Password, diags } -func HandleTrafficFilterChange(ctx context.Context, client *api.API, plan, state v2.DeploymentTF) diag.Diagnostics { - if plan.TrafficFilter.Equal(state.TrafficFilter) { - return nil - } +func HandleTrafficFilterChange(ctx context.Context, client *api.API, plan v2.DeploymentTF, stateRules ruleSet) ([]string, diag.Diagnostics) { + // if plan.TrafficFilter.Equal(state.TrafficFilter) { + // return nil + // } - var planRules, stateRules ruleSet + var planRules ruleSet if diags := plan.TrafficFilter.ElementsAs(ctx, &planRules, true); diags.HasError() { - return diags - } - - if diags := state.TrafficFilter.ElementsAs(ctx, &stateRules, true); diags.HasError() { - return diags + return []string{}, diags } var rulesToAdd, rulesToDelete []string @@ -157,7 +159,7 @@ func HandleTrafficFilterChange(ctx context.Context, client *api.API, plan, state } } - return diags + return planRules, diags } type ruleSet []string