ec/ecresource/deploymentresource/read.go (234 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"
"errors"
"fmt"
"github.com/blang/semver"
"github.com/elastic/cloud-sdk-go/pkg/api/apierror"
"github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi"
"github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/deputil"
"github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/eskeystoreapi"
"github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/esremoteclustersapi"
"github.com/elastic/cloud-sdk-go/pkg/client/deployments"
"github.com/elastic/cloud-sdk-go/pkg/models"
apmv2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/apm/v2"
deploymentv2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/deployment/v2"
elasticsearchv2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/elasticsearch/v2"
enterprisesearchv2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/enterprisesearch/v2"
integrationsserverv2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/integrationsserver/v2"
kibanav2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/kibana/v2"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
)
func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
if !r.ready(&response.Diagnostics) {
return
}
var curState deploymentv2.DeploymentTF
diags := request.State.Get(ctx, &curState)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
var newState *deploymentv2.Deployment
privateFilters, d := readPrivateStateTrafficFilters(ctx, request.Private)
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, privateFilters, response)
response.Diagnostics.Append(diags...)
if newState == nil {
response.State.RemoveResource(ctx)
}
if newState != nil {
diags = response.State.Set(ctx, newState)
}
response.Diagnostics.Append(diags...)
}
// 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, privateFilters []string, readResponse *resource.ReadResponse) (*deploymentv2.Deployment, diag.Diagnostics) {
var diags diag.Diagnostics
var base deploymentv2.DeploymentTF
switch {
case plan != nil:
base = *plan
case state != nil:
base = *state
default:
diags.AddError("both state and plan are empty", "please specify at least one of them")
return nil, diags
}
response, err := deploymentapi.Get(deploymentapi.GetParams{
API: r.client,
DeploymentID: id,
QueryParams: deputil.QueryParams{
ShowSettings: true,
ShowPlans: true,
ShowMetadata: true,
ShowPlanDefaults: true,
ShowInstanceConfigurations: true,
},
})
if err != nil {
if deploymentNotFound(err) {
diags.AddError("Deployment not found", err.Error())
return nil, diags
}
diags.AddError("Deployment get error", err.Error())
return nil, diags
}
if readResponse != nil {
UpdatePrivateStateInstanceConfigurations(ctx, readResponse.Private, response.InstanceConfigurations)
}
if !HasRunningResources(response) {
return nil, nil
}
if response.Resources == nil || len(response.Resources.Elasticsearch) == 0 {
diags.AddError("Get resource error", "cannot find Elasticsearch in response resources")
return nil, diags
}
if response.Resources.Elasticsearch[0].Info.PlanInfo.Current != nil && response.Resources.Elasticsearch[0].Info.PlanInfo.Current.Plan != nil {
if err := checkVersion(response.Resources.Elasticsearch[0].Info.PlanInfo.Current.Plan.Elasticsearch.Version); err != nil {
diags.AddError("Get resource error", err.Error())
return nil, diags
}
}
refId := ""
var baseElasticsearch *elasticsearchv2.ElasticsearchTF
if diags = tfsdk.ValueAs(ctx, base.Elasticsearch, &baseElasticsearch); diags.HasError() {
return nil, diags
}
if baseElasticsearch != nil {
refId = baseElasticsearch.RefId.ValueString()
}
remotes, err := esremoteclustersapi.Get(esremoteclustersapi.GetParams{
API: r.client, DeploymentID: id,
RefID: refId,
})
if err != nil {
diags.AddError("Remote clusters read error", err.Error())
return nil, diags
}
if remotes == nil {
remotes = &models.RemoteResources{}
}
deployment, err := deploymentv2.ReadDeployment(response, remotes, deploymentResources)
if err != nil {
diags.AddError("Deployment read error", err.Error())
return nil, diags
}
deployment.RequestId = base.RequestId.ValueString()
if !base.ResetElasticsearchPassword.IsNull() && !base.ResetElasticsearchPassword.IsUnknown() {
deployment.ResetElasticsearchPassword = base.ResetElasticsearchPassword.ValueBoolPointer()
}
if !base.MigrateToLatestHardware.IsNull() && !base.MigrateToLatestHardware.IsUnknown() {
deployment.MigrateToLatestHardware = base.MigrateToLatestHardware.ValueBoolPointer()
}
diags.Append(deployment.IncludePrivateStateTrafficFilters(ctx, base, privateFilters)...)
deployment.SetCredentialsIfEmpty(state)
diags.Append(deployment.ProcessSelfInObservability(ctx, base)...)
deployment.NullifyUnusedEsTopologies(ctx, baseElasticsearch)
diags.Append(deployment.PersistSnapshotSource(ctx, baseElasticsearch)...)
if !deployment.HasNodeTypes() {
// The MigrateDeploymentTemplate request can only be performed for deployments that use node roles.
// We'll skip this logic for deployments with node types.
migrateTemplateRequest, err := r.client.V1API.Deployments.MigrateDeploymentTemplate(
deployments.NewMigrateDeploymentTemplateParams().WithDeploymentID(deployment.Id).WithTemplateID(deployment.DeploymentTemplateId),
r.client.AuthWriter,
)
if err != nil {
diags.AddError("Template migrate request error", err.Error())
return nil, diags
}
// Store migrate request in private state
if readResponse != nil {
UpdatePrivateStateMigrateTemplateRequest(ctx, readResponse.Private, migrateTemplateRequest)
}
deployment.SetLatestInstanceConfigInfo(migrateTemplateRequest)
} else {
// Set latest_instance_configuration_* fields to current values
// If this isn't done, when migrating a deployment to node roles, these fields will contain inconsistent values
deployment.SetLatestInstanceConfigInfoToCurrent()
}
// Set Elasticsearch `strategy` to the one from plan.
// We don't care about backend current `strategy`'s value and should not trigger a change,
// if the backend's value differs from the local state.
if baseElasticsearch != nil && !baseElasticsearch.Strategy.IsNull() {
deployment.Elasticsearch.Strategy = baseElasticsearch.Strategy.ValueStringPointer()
}
// sync Elasticsearch keystore contents if plan or state defines it:
// - all keystore entries that are not managed by the resource are left alone
// - if backend doesn't contain some keystore entry, the entry should be removed from the future state as well
if baseElasticsearch != nil && deployment.Elasticsearch != nil && !baseElasticsearch.KeystoreContents.IsNull() {
ds := baseElasticsearch.KeystoreContents.ElementsAs(ctx, &deployment.Elasticsearch.KeystoreContents, true)
diags.Append(ds...)
keystoreContents, err := eskeystoreapi.Get(eskeystoreapi.GetParams{
API: r.client,
DeploymentID: id,
})
if err != nil {
diags.AddError("Deployment keystore read error", err.Error())
return nil, diags
}
for entryName, entryVal := range deployment.Elasticsearch.KeystoreContents {
secret, ok := keystoreContents.Secrets[entryName]
if !ok {
delete(deployment.Elasticsearch.KeystoreContents, entryName)
continue
}
if secret.AsFile != nil {
entryVal.AsFile = secret.AsFile
deployment.Elasticsearch.KeystoreContents[entryName] = entryVal
}
}
}
// ReadDeployment returns empty config struct if there is no config, so we have to nullify it if plan doesn't contain it
// we use state for plan in Read and there is no state during import so we need to check elasticsearchPlan against nil
if baseElasticsearch != nil &&
baseElasticsearch.Config.IsNull() &&
deployment.Elasticsearch != nil &&
deployment.Elasticsearch.Config != nil &&
deployment.Elasticsearch.Config.IsEmpty() {
deployment.Elasticsearch.Config = nil
}
return deployment, diags
}
func deploymentNotFound(err error) bool {
// We're using the As() call since we do not care about the error value
// but do care about the error's contents type since it's an implicit 404.
var notDeploymentNotFound *deployments.GetDeploymentNotFound
if errors.As(err, ¬DeploymentNotFound) {
return true
}
// We also check for the case where a 403 is thrown for ESS.
return apierror.IsRuntimeStatusCode(err, 403)
}
var minimumSupportedVersion = semver.MustParse("6.6.0")
func checkVersion(version string) error {
v, err := semver.New(version)
if err != nil {
return fmt.Errorf("unable to parse deployment version: %w", err)
}
if v.LT(minimumSupportedVersion) {
return fmt.Errorf(
`invalid deployment version "%s": minimum supported version is "%s"`,
v.String(), minimumSupportedVersion.String(),
)
}
return nil
}
func HasRunningResources(res *models.DeploymentGetResponse) bool {
if res.Resources != nil {
for _, r := range res.Resources.Elasticsearch {
if !elasticsearchv2.IsElasticsearchStopped(r) {
return true
}
}
for _, r := range res.Resources.Kibana {
if !kibanav2.IsKibanaStopped(r) {
return true
}
}
for _, r := range res.Resources.Apm {
if !apmv2.IsApmStopped(r) {
return true
}
}
for _, r := range res.Resources.EnterpriseSearch {
if !enterprisesearchv2.IsEnterpriseSearchStopped(r) {
return true
}
}
for _, r := range res.Resources.IntegrationsServer {
if !integrationsserverv2.IsIntegrationsServerStopped(r) {
return true
}
}
}
return false
}