diff --git a/.changelog/788.txt b/.changelog/788.txt new file mode 100644 index 000000000..3b326a7f7 --- /dev/null +++ b/.changelog/788.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/deployment: Persist the snapshot source settings during reads. This fixes a [provider crash](https://github.com/elastic/terraform-provider-ec/issues/787) when creating a deployment from a snapshot. +``` diff --git a/ec/ecresource/deploymentresource/deployment/v2/deployment_read.go b/ec/ecresource/deploymentresource/deployment/v2/deployment_read.go index 8fbbd0622..f052872e6 100644 --- a/ec/ecresource/deploymentresource/deployment/v2/deployment_read.go +++ b/ec/ecresource/deploymentresource/deployment/v2/deployment_read.go @@ -21,15 +21,17 @@ import ( "context" "errors" "fmt" - "github.com/elastic/cloud-sdk-go/pkg/client/deployments" "slices" "strings" + "github.com/elastic/cloud-sdk-go/pkg/client/deployments" + "github.com/blang/semver" "github.com/elastic/cloud-sdk-go/pkg/models" "github.com/elastic/cloud-sdk-go/pkg/util/ec" apmv2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/apm/v2" + elasticsearchv1 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/elasticsearch/v1" 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" @@ -39,6 +41,7 @@ import ( "github.com/elastic/terraform-provider-ec/ec/internal/converters" "github.com/elastic/terraform-provider-ec/ec/internal/util" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -65,6 +68,28 @@ type Deployment struct { MigrateToLatestHardware *bool `tfsdk:"migrate_to_latest_hardware"` } +func (dep *Deployment) PersistSnapshotSource(ctx context.Context, esPlan *elasticsearchv2.ElasticsearchTF) diag.Diagnostics { + if dep == nil || dep.Elasticsearch == nil { + return nil + } + + if esPlan == nil || esPlan.SnapshotSource.IsNull() || esPlan.SnapshotSource.IsUnknown() { + return nil + } + + var snapshotSource *elasticsearchv1.ElasticsearchSnapshotSourceTF + if diags := tfsdk.ValueAs(ctx, esPlan.SnapshotSource, &snapshotSource); diags.HasError() { + return diags + } + + dep.Elasticsearch.SnapshotSource = &elasticsearchv2.ElasticsearchSnapshotSource{ + SourceElasticsearchClusterId: snapshotSource.SourceElasticsearchClusterId.ValueString(), + SnapshotName: snapshotSource.SnapshotName.ValueString(), + } + + return nil +} + // Nullify Elasticsearch topologies that have zero size and are not specified in plan func (dep *Deployment) NullifyUnusedEsTopologies(ctx context.Context, esPlan *elasticsearchv2.ElasticsearchTF) { if dep.Elasticsearch == nil { diff --git a/ec/ecresource/deploymentresource/deployment/v2/deployment_read_test.go b/ec/ecresource/deploymentresource/deployment/v2/deployment_read_test.go index 12144a093..f10ca629a 100644 --- a/ec/ecresource/deploymentresource/deployment/v2/deployment_read_test.go +++ b/ec/ecresource/deploymentresource/deployment/v2/deployment_read_test.go @@ -18,6 +18,7 @@ package v2 import ( + "context" "errors" "testing" @@ -25,11 +26,15 @@ import ( "github.com/elastic/cloud-sdk-go/pkg/models" "github.com/elastic/cloud-sdk-go/pkg/util/ec" apmv2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/apm/v2" + elasticsearchv1 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/elasticsearch/v1" elasticsearchv2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/elasticsearch/v2" enterprisesearchv2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/enterprisesearch/v2" kibanav2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/kibana/v2" observabilityv2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/observability/v2" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_readDeployment(t *testing.T) { @@ -1723,3 +1728,59 @@ func Test_getDeploymentTemplateID(t *testing.T) { }) } } + +func Test_PersistSnapshotSource(t *testing.T) { + tests := []struct { + name string + deployment *Deployment + snapshotSource *elasticsearchv1.ElasticsearchSnapshotSource + expectedSourceElasticsearchClusterId string + expectedSnapshotName string + }{ + { + name: "should noop if deployment is nil", + }, + { + name: "should noop if the esplan snapshot source is null", + deployment: &Deployment{}, + }, + { + name: "should set the snapshot source cluster and snapshot name if specified in the plan", + deployment: &Deployment{ + Elasticsearch: &elasticsearchv2.Elasticsearch{}, + }, + snapshotSource: &elasticsearchv1.ElasticsearchSnapshotSource{ + SourceElasticsearchClusterId: "source-cluster-id", + SnapshotName: "snapshot-name", + }, + expectedSourceElasticsearchClusterId: "source-cluster-id", + expectedSnapshotName: "snapshot-name", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj types.Object + if tt.snapshotSource != nil { + diags := tfsdk.ValueFrom(context.Background(), tt.snapshotSource, elasticsearchv2.ElasticsearchSnapshotSourceSchema().GetType(), &obj) + require.Nil(t, diags) + } + + esPlan := elasticsearchv2.ElasticsearchTF{ + SnapshotSource: obj, + } + + diags := tt.deployment.PersistSnapshotSource(context.Background(), &esPlan) + require.Nil(t, diags) + + var snapshotName, sourceESClusterID string + if tt.deployment != nil && tt.deployment.Elasticsearch != nil && tt.deployment.Elasticsearch.SnapshotSource != nil { + snapshotName = tt.deployment.Elasticsearch.SnapshotSource.SnapshotName + sourceESClusterID = tt.deployment.Elasticsearch.SnapshotSource.SourceElasticsearchClusterId + } + + require.Equal(t, tt.expectedSnapshotName, snapshotName) + require.Equal(t, tt.expectedSourceElasticsearchClusterId, sourceESClusterID) + }) + } +} diff --git a/ec/ecresource/deploymentresource/elasticsearch/v2/schema.go b/ec/ecresource/deploymentresource/elasticsearch/v2/schema.go index 9f01eb0ea..1f4ebc9ee 100644 --- a/ec/ecresource/deploymentresource/elasticsearch/v2/schema.go +++ b/ec/ecresource/deploymentresource/elasticsearch/v2/schema.go @@ -141,7 +141,7 @@ func ElasticsearchSchema() schema.Attribute { "snapshot": elasticsearchSnapshotSchema(), - "snapshot_source": elasticsearchSnapshotSourceSchema(), + "snapshot_source": ElasticsearchSnapshotSourceSchema(), "extension": elasticsearchExtensionSchema(), @@ -361,7 +361,7 @@ func elasticsearchSnapshotRepositoryReferenceSchema() schema.Attribute { } } -func elasticsearchSnapshotSourceSchema() schema.Attribute { +func ElasticsearchSnapshotSourceSchema() schema.Attribute { return schema.SingleNestedAttribute{ Description: `Restores data from a snapshot of another deployment. diff --git a/ec/ecresource/deploymentresource/read.go b/ec/ecresource/deploymentresource/read.go index 32e3cb118..61d450edc 100644 --- a/ec/ecresource/deploymentresource/read.go +++ b/ec/ecresource/deploymentresource/read.go @@ -177,6 +177,7 @@ func (r *Resource) read(ctx context.Context, id string, state *deploymentv2.Depl deployment.ProcessSelfInObservability() 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.