From dcb628a1c2135e8094bdc35183c7852419217b13 Mon Sep 17 00:00:00 2001 From: Gil Raphaelli Date: Mon, 5 Jan 2026 15:05:05 -0500 Subject: [PATCH 1/8] add serverless project import --- docs/resources/elasticsearch_project.md | 2 + docs/resources/observability_project.md | 2 + docs/resources/security_project.md | 2 + ec/acc/elasticsearch_project_test.go | 59 +++++++- ec/acc/observability_project_test.go | 54 +++++++ ec/acc/security_project_test.go | 59 +++++++- .../projectresource/elasticsearch.go | 4 + ec/ecresource/projectresource/import.go | 51 +++++++ ec/ecresource/projectresource/mocks.gen.go | 14 ++ .../projectresource/observability.go | 4 + ec/ecresource/projectresource/resource.go | 2 + .../projectresource/resource_test.go | 136 ++++++++++++++++++ ec/ecresource/projectresource/security.go | 4 + .../resources/elasticsearch_project.md.tmpl | 2 + .../resources/observability_project.md.tmpl | 2 + templates/resources/security_project.md.tmpl | 2 + 16 files changed, 393 insertions(+), 6 deletions(-) create mode 100644 ec/ecresource/projectresource/import.go diff --git a/docs/resources/elasticsearch_project.md b/docs/resources/elasticsearch_project.md index 4ff306a49..5c09fb56c 100644 --- a/docs/resources/elasticsearch_project.md +++ b/docs/resources/elasticsearch_project.md @@ -90,3 +90,5 @@ Projects can be imported using the `id`, for example: ```shell terraform import ec_elasticsearch_project.id 320b7b540dfc967a7a649c18e2fce4ed ``` + +~> **Note on Credentials** The `credentials` attribute (containing `username` and `password`) is only available when the project is first created. When importing an existing project, these credentials will not be available in the Terraform state as the API does not return them on read operations. diff --git a/docs/resources/observability_project.md b/docs/resources/observability_project.md index 33448e065..1441a4d15 100644 --- a/docs/resources/observability_project.md +++ b/docs/resources/observability_project.md @@ -80,3 +80,5 @@ Projects can be imported using the `id`, for example: ```shell terraform import ec_observability_project.id 320b7b540dfc967a7a649c18e2fce4ed ``` + +~> **Note on Credentials** The `credentials` attribute (containing `username` and `password`) is only available when the project is first created. When importing an existing project, these credentials will not be available in the Terraform state as the API does not return them on read operations. diff --git a/docs/resources/security_project.md b/docs/resources/security_project.md index d72fe8ee1..018e873b3 100644 --- a/docs/resources/security_project.md +++ b/docs/resources/security_project.md @@ -90,3 +90,5 @@ Projects can be imported using the `id`, for example: ```shell terraform import ec_security_project.id 320b7b540dfc967a7a649c18e2fce4ed ``` + +~> **Note on Credentials** The `credentials` attribute (containing `username` and `password`) is only available when the project is first created. When importing an existing project, these credentials will not be available in the Terraform state as the API does not return them on read operations. diff --git a/ec/acc/elasticsearch_project_test.go b/ec/acc/elasticsearch_project_test.go index f01b6f69e..c57454c01 100644 --- a/ec/acc/elasticsearch_project_test.go +++ b/ec/acc/elasticsearch_project_test.go @@ -81,10 +81,17 @@ func TestAccElasticsearchProject(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "credentials.username"), resource.TestCheckResourceAttrSet(resourceName, "credentials.password"), resource.TestCheckResourceAttrSet(resourceName, "cloud_id"), - ), - }, + ), }, - }) + { + // Test import. + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"credentials"}, + }, + }, +}) } func testAccBasicElasticsearchProject(id string, name string, region string) string { @@ -106,6 +113,52 @@ resource ec_elasticsearch_project "%s" { `, id, name, region, alias) } +func TestAccElasticsearchProjectImport(t *testing.T) { + resId := "import_project" + resourceName := fmt.Sprintf("ec_elasticsearch_project.%s", resId) + randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + region := getRegion() + if !strings.HasPrefix("aws-", region) { + region = fmt.Sprintf("aws-%s", region) + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviderFactory, + CheckDestroy: testAccElasticsearchProjectDestroy, + Steps: []resource.TestStep{ + { + // Create a project to import. + Config: testAccBasicElasticsearchProject(resId, randomName, region), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", randomName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + ), + }, + { + // Import the project and verify all attributes. + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"credentials"}, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", randomName), + resource.TestCheckResourceAttr(resourceName, "region_id", region), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "alias"), + resource.TestCheckResourceAttrSet(resourceName, "cloud_id"), + resource.TestCheckResourceAttrSet(resourceName, "endpoints.elasticsearch"), + resource.TestCheckResourceAttrSet(resourceName, "endpoints.kibana"), + resource.TestCheckResourceAttrSet(resourceName, "metadata.created_at"), + resource.TestCheckResourceAttrSet(resourceName, "metadata.created_by"), + resource.TestCheckResourceAttrSet(resourceName, "metadata.organization_id"), + resource.TestCheckResourceAttr(resourceName, "type", "elasticsearch"), + ), + }, + }, + }) +} + func testAccElasticsearchProjectDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration client, err := newServerlessAPI() diff --git a/ec/acc/observability_project_test.go b/ec/acc/observability_project_test.go index de4bd0a5c..4f800da82 100644 --- a/ec/acc/observability_project_test.go +++ b/ec/acc/observability_project_test.go @@ -91,6 +91,13 @@ func TestAccObservabilityProject(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "cloud_id"), ), }, + { + // Test import. + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"credentials"}, + }, }, }) } @@ -179,6 +186,53 @@ resource ec_observability_project "%s" { `, id, name, region, productTier) } +func TestAccObservabilityProjectImport(t *testing.T) { + resId := "import_project" + resourceName := fmt.Sprintf("ec_observability_project.%s", resId) + randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + region := getRegion() + if !strings.HasPrefix("aws-", region) { + region = fmt.Sprintf("aws-%s", region) + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviderFactory, + CheckDestroy: testAccObservabilityProjectDestroy, + Steps: []resource.TestStep{ + { + // Create a project to import. + Config: testAccBasicObservabilityProject(resId, randomName, region), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", randomName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + ), + }, + { + // Import the project and verify all attributes. + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"credentials"}, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", randomName), + resource.TestCheckResourceAttr(resourceName, "region_id", region), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "alias"), + resource.TestCheckResourceAttrSet(resourceName, "cloud_id"), + resource.TestCheckResourceAttrSet(resourceName, "endpoints.elasticsearch"), + resource.TestCheckResourceAttrSet(resourceName, "endpoints.kibana"), + resource.TestCheckResourceAttrSet(resourceName, "endpoints.apm"), + resource.TestCheckResourceAttrSet(resourceName, "metadata.created_at"), + resource.TestCheckResourceAttrSet(resourceName, "metadata.created_by"), + resource.TestCheckResourceAttrSet(resourceName, "metadata.organization_id"), + resource.TestCheckResourceAttr(resourceName, "type", "observability"), + ), + }, + }, + }) +} + func testAccObservabilityProjectDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration client, err := newServerlessAPI() diff --git a/ec/acc/security_project_test.go b/ec/acc/security_project_test.go index 3db2a90b8..2519d5930 100644 --- a/ec/acc/security_project_test.go +++ b/ec/acc/security_project_test.go @@ -81,10 +81,17 @@ func TestAccSecurityProject(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "credentials.username"), resource.TestCheckResourceAttrSet(resourceName, "credentials.password"), resource.TestCheckResourceAttrSet(resourceName, "cloud_id"), - ), - }, + ), }, - }) + { + // Test import. + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"credentials"}, + }, + }, +}) } func TestAccSecurityProjectWithAdminFeaturesAndProductTypes(t *testing.T) { @@ -156,6 +163,52 @@ resource ec_security_project "%s" { `, id, name, region, adminPackage) } +func TestAccSecurityProjectImport(t *testing.T) { + resId := "import_project" + resourceName := fmt.Sprintf("ec_security_project.%s", resId) + randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + region := getRegion() + if !strings.HasPrefix("aws-", region) { + region = fmt.Sprintf("aws-%s", region) + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviderFactory, + CheckDestroy: testAccSecurityProjectDestroy, + Steps: []resource.TestStep{ + { + // Create a project to import. + Config: testAccBasicSecurityProject(resId, randomName, region), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", randomName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + ), + }, + { + // Import the project and verify all attributes. + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"credentials"}, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", randomName), + resource.TestCheckResourceAttr(resourceName, "region_id", region), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "alias"), + resource.TestCheckResourceAttrSet(resourceName, "cloud_id"), + resource.TestCheckResourceAttrSet(resourceName, "endpoints.elasticsearch"), + resource.TestCheckResourceAttrSet(resourceName, "endpoints.kibana"), + resource.TestCheckResourceAttrSet(resourceName, "metadata.created_at"), + resource.TestCheckResourceAttrSet(resourceName, "metadata.created_by"), + resource.TestCheckResourceAttrSet(resourceName, "metadata.organization_id"), + resource.TestCheckResourceAttr(resourceName, "type", "security"), + ), + }, + }, + }) +} + func testAccSecurityProjectDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration client, err := newServerlessAPI() diff --git a/ec/ecresource/projectresource/elasticsearch.go b/ec/ecresource/projectresource/elasticsearch.go index 0c528adf1..6fa634292 100644 --- a/ec/ecresource/projectresource/elasticsearch.go +++ b/ec/ecresource/projectresource/elasticsearch.go @@ -57,6 +57,10 @@ func (es elasticsearchModelReader) GetID(model resource_elasticsearch_project.El return model.Id.ValueString() } +func (es elasticsearchModelReader) NewEmptyModel() resource_elasticsearch_project.ElasticsearchProjectModel { + return resource_elasticsearch_project.ElasticsearchProjectModel{} +} + func (es elasticsearchModelReader) Modify(plan resource_elasticsearch_project.ElasticsearchProjectModel, state resource_elasticsearch_project.ElasticsearchProjectModel, cfg resource_elasticsearch_project.ElasticsearchProjectModel) resource_elasticsearch_project.ElasticsearchProjectModel { plan.Credentials = useStateForUnknown(plan.Credentials, state.Credentials) plan.Endpoints = useStateForUnknown(plan.Endpoints, state.Endpoints) diff --git a/ec/ecresource/projectresource/import.go b/ec/ecresource/projectresource/import.go new file mode 100644 index 000000000..b3333d2f8 --- /dev/null +++ b/ec/ecresource/projectresource/import.go @@ -0,0 +1,51 @@ +// 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 projectresource + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *Resource[T]) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + if !resourceReady(r, &response.Diagnostics) { + return + } + + projectID := request.ID + + // Create an empty model to populate with data from the API + emptyModel := r.modelHandler.NewEmptyModel() + + found, model, diags := r.api.Read(ctx, projectID, emptyModel) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + if !found { + response.Diagnostics.AddError( + "Resource not found", + "No project was found with the specified ID: "+projectID, + ) + return + } + + response.Diagnostics.Append(response.State.Set(ctx, model)...) +} diff --git a/ec/ecresource/projectresource/mocks.gen.go b/ec/ecresource/projectresource/mocks.gen.go index f1430c7e5..f7ec8041f 100644 --- a/ec/ecresource/projectresource/mocks.gen.go +++ b/ec/ecresource/projectresource/mocks.gen.go @@ -151,6 +151,20 @@ func (mr *MockmodelHandlerMockRecorder[T]) Schema(arg0, arg1, arg2 any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Schema", reflect.TypeOf((*MockmodelHandler[T])(nil).Schema), arg0, arg1, arg2) } +// NewEmptyModel mocks base method. +func (m *MockmodelHandler[T]) NewEmptyModel() T { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewEmptyModel") + ret0, _ := ret[0].(T) + return ret0 +} + +// NewEmptyModel indicates an expected call of NewEmptyModel. +func (mr *MockmodelHandlerMockRecorder[T]) NewEmptyModel() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewEmptyModel", reflect.TypeOf((*MockmodelHandler[T])(nil).NewEmptyModel)) +} + // Mockapi is a mock of api interface. type Mockapi[TModel any] struct { ctrl *gomock.Controller diff --git a/ec/ecresource/projectresource/observability.go b/ec/ecresource/projectresource/observability.go index d09369afe..b0ac39cc4 100644 --- a/ec/ecresource/projectresource/observability.go +++ b/ec/ecresource/projectresource/observability.go @@ -55,6 +55,10 @@ func (obs observabilityModelReader) GetID(model resource_observability_project.O return model.Id.ValueString() } +func (obs observabilityModelReader) NewEmptyModel() resource_observability_project.ObservabilityProjectModel { + return resource_observability_project.ObservabilityProjectModel{} +} + func (obs observabilityModelReader) Modify(plan resource_observability_project.ObservabilityProjectModel, state resource_observability_project.ObservabilityProjectModel, cfg resource_observability_project.ObservabilityProjectModel) resource_observability_project.ObservabilityProjectModel { plan.Credentials = useStateForUnknown(plan.Credentials, state.Credentials) plan.Endpoints = useStateForUnknown(plan.Endpoints, state.Endpoints) diff --git a/ec/ecresource/projectresource/resource.go b/ec/ecresource/projectresource/resource.go index 0288eb4e6..86bcb1c81 100644 --- a/ec/ecresource/projectresource/resource.go +++ b/ec/ecresource/projectresource/resource.go @@ -33,6 +33,7 @@ import ( var _ resource.Resource = &Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{} var _ resource.ResourceWithConfigure = &Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{} var _ resource.ResourceWithModifyPlan = &Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{} +var _ resource.ResourceWithImportState = &Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{} type Resource[T any] struct { modelHandler modelHandler[T] @@ -52,6 +53,7 @@ type modelHandler[T any] interface { ReadFrom(context.Context, modelGetter) (*T, diag.Diagnostics) GetID(T) string Modify(T, T, T) T + NewEmptyModel() T } type api[TModel any] interface { diff --git a/ec/ecresource/projectresource/resource_test.go b/ec/ecresource/projectresource/resource_test.go index d69d9c0ee..f7f9b01bb 100644 --- a/ec/ecresource/projectresource/resource_test.go +++ b/ec/ecresource/projectresource/resource_test.go @@ -25,6 +25,7 @@ import ( "github.com/elastic/terraform-provider-ec/ec/internal" "github.com/elastic/terraform-provider-ec/ec/internal/gen/serverless/mocks" "github.com/elastic/terraform-provider-ec/ec/internal/gen/serverless/resource_elasticsearch_project" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -182,3 +183,138 @@ func TestModifyPlan(t *testing.T) { require.Equal(t, planModel.Id.ValueString(), id) }) } + +func TestImportState(t *testing.T) { + ctrl := gomock.NewController(t) + + t.Run("should error when api is not ready", func(t *testing.T) { + ctx := context.Background() + req := resource.ImportStateRequest{ + ID: "project-id", + } + res := resource.ImportStateResponse{ + State: tfsdk.State{ + Schema: resource_elasticsearch_project.ElasticsearchProjectResourceSchema(ctx), + }, + } + + mockApi := NewMockapi[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) + mockApi.EXPECT().Ready().Return(false) + + r := Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{ + api: mockApi, + } + r.ImportState(ctx, req, &res) + + require.True(t, res.Diagnostics.HasError()) + require.Contains(t, res.Diagnostics.Errors()[0].Summary(), "Unconfigured API Client") + }) + + t.Run("should error when project is not found", func(t *testing.T) { + ctx := context.Background() + projectID := "non-existent-project-id" + req := resource.ImportStateRequest{ + ID: projectID, + } + res := resource.ImportStateResponse{ + State: tfsdk.State{ + Schema: resource_elasticsearch_project.ElasticsearchProjectResourceSchema(ctx), + }, + } + + emptyModel := resource_elasticsearch_project.ElasticsearchProjectModel{} + + mockApi := NewMockapi[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) + mockApi.EXPECT().Ready().Return(true) + mockApi.EXPECT().Read(ctx, projectID, emptyModel).Return(false, emptyModel, nil) + + mockHandler := NewMockmodelHandler[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) + mockHandler.EXPECT().NewEmptyModel().Return(emptyModel) + + r := Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{ + api: mockApi, + modelHandler: mockHandler, + } + r.ImportState(ctx, req, &res) + + require.True(t, res.Diagnostics.HasError()) + require.Contains(t, res.Diagnostics.Errors()[0].Summary(), "Resource not found") + }) + + t.Run("should error when api read fails", func(t *testing.T) { + ctx := context.Background() + projectID := "project-id" + req := resource.ImportStateRequest{ + ID: projectID, + } + res := resource.ImportStateResponse{ + State: tfsdk.State{ + Schema: resource_elasticsearch_project.ElasticsearchProjectResourceSchema(ctx), + }, + } + + emptyModel := resource_elasticsearch_project.ElasticsearchProjectModel{} + + mockApi := NewMockapi[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) + mockApi.EXPECT().Ready().Return(true) + mockApi.EXPECT().Read(ctx, projectID, emptyModel).Return(false, emptyModel, diag.Diagnostics{ + diag.NewErrorDiagnostic("API Error", "Failed to read project"), + }) + + mockHandler := NewMockmodelHandler[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) + mockHandler.EXPECT().NewEmptyModel().Return(emptyModel) + + r := Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{ + api: mockApi, + modelHandler: mockHandler, + } + r.ImportState(ctx, req, &res) + + require.True(t, res.Diagnostics.HasError()) + require.Contains(t, res.Diagnostics.Errors()[0].Summary(), "API Error") + }) + + t.Run("should successfully import project", func(t *testing.T) { + ctx := context.Background() + projectID := "project-id" + req := resource.ImportStateRequest{ + ID: projectID, + } + res := resource.ImportStateResponse{ + State: tfsdk.State{ + Schema: resource_elasticsearch_project.ElasticsearchProjectResourceSchema(ctx), + }, + } + + emptyModel := resource_elasticsearch_project.ElasticsearchProjectModel{} + readModel := resource_elasticsearch_project.ElasticsearchProjectModel{ + Id: types.StringValue(projectID), + Name: types.StringValue("imported-project"), + RegionId: types.StringValue("us-east-1"), + } + + mockApi := NewMockapi[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) + mockApi.EXPECT().Ready().Return(true) + mockApi.EXPECT().Read(ctx, projectID, emptyModel).Return(true, readModel, nil) + + mockHandler := NewMockmodelHandler[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) + mockHandler.EXPECT().NewEmptyModel().Return(emptyModel) + + r := Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{ + api: mockApi, + modelHandler: mockHandler, + } + r.ImportState(ctx, req, &res) + + require.False(t, res.Diagnostics.HasError()) + + // Validate that the imported values were set in the state + var id string + res.State.GetAttribute(ctx, path.Root("id"), &id) + require.Equal(t, projectID, id) + + var name string + res.State.GetAttribute(ctx, path.Root("name"), &name) + require.Equal(t, "imported-project", name) + }) +} diff --git a/ec/ecresource/projectresource/security.go b/ec/ecresource/projectresource/security.go index 2a9f60d51..ef17da9e3 100644 --- a/ec/ecresource/projectresource/security.go +++ b/ec/ecresource/projectresource/security.go @@ -74,6 +74,10 @@ func (sec securityModelReader) GetID(model resource_security_project.SecurityPro return model.Id.ValueString() } +func (sec securityModelReader) NewEmptyModel() resource_security_project.SecurityProjectModel { + return resource_security_project.SecurityProjectModel{} +} + func (sec securityModelReader) Modify(plan resource_security_project.SecurityProjectModel, state resource_security_project.SecurityProjectModel, cfg resource_security_project.SecurityProjectModel) resource_security_project.SecurityProjectModel { plan.Credentials = useStateForUnknown(plan.Credentials, state.Credentials) plan.Endpoints = useStateForUnknown(plan.Endpoints, state.Endpoints) diff --git a/templates/resources/elasticsearch_project.md.tmpl b/templates/resources/elasticsearch_project.md.tmpl index 6c0510c4a..455b16fdc 100644 --- a/templates/resources/elasticsearch_project.md.tmpl +++ b/templates/resources/elasticsearch_project.md.tmpl @@ -24,3 +24,5 @@ Elastic will work to fix any issues, but features in technical preview are not s Projects can be imported using the `id`, for example: {{ codefile "shell" .ImportFile }} + +~> **Note on Credentials** The `credentials` attribute (containing `username` and `password`) is only available when the project is first created. When importing an existing project, these credentials will not be available in the Terraform state as the API does not return them on read operations. diff --git a/templates/resources/observability_project.md.tmpl b/templates/resources/observability_project.md.tmpl index ad1dedccb..f93b1392c 100644 --- a/templates/resources/observability_project.md.tmpl +++ b/templates/resources/observability_project.md.tmpl @@ -24,3 +24,5 @@ Elastic will work to fix any issues, but features in technical preview are not s Projects can be imported using the `id`, for example: {{ codefile "shell" .ImportFile }} + +~> **Note on Credentials** The `credentials` attribute (containing `username` and `password`) is only available when the project is first created. When importing an existing project, these credentials will not be available in the Terraform state as the API does not return them on read operations. diff --git a/templates/resources/security_project.md.tmpl b/templates/resources/security_project.md.tmpl index 799b3e25d..2ac144ec4 100644 --- a/templates/resources/security_project.md.tmpl +++ b/templates/resources/security_project.md.tmpl @@ -24,3 +24,5 @@ Elastic will work to fix any issues, but features in technical preview are not s Projects can be imported using the `id`, for example: {{ codefile "shell" .ImportFile }} + +~> **Note on Credentials** The `credentials` attribute (containing `username` and `password`) is only available when the project is first created. When importing an existing project, these credentials will not be available in the Terraform state as the API does not return them on read operations. From 0497419e6a2af766cf27352b67e53a69eb8dcce9 Mon Sep 17 00:00:00 2001 From: Gil Raphaelli Date: Mon, 5 Jan 2026 15:56:01 -0500 Subject: [PATCH 2/8] lint --- ec/acc/elasticsearch_project_test.go | 20 ++++++++++---------- ec/acc/security_project_test.go | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/ec/acc/elasticsearch_project_test.go b/ec/acc/elasticsearch_project_test.go index c57454c01..30365c810 100644 --- a/ec/acc/elasticsearch_project_test.go +++ b/ec/acc/elasticsearch_project_test.go @@ -81,17 +81,17 @@ func TestAccElasticsearchProject(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "credentials.username"), resource.TestCheckResourceAttrSet(resourceName, "credentials.password"), resource.TestCheckResourceAttrSet(resourceName, "cloud_id"), - ), - }, - { - // Test import. - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"credentials"}, + ), + }, + { + // Test import. + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"credentials"}, + }, }, - }, -}) + }) } func testAccBasicElasticsearchProject(id string, name string, region string) string { diff --git a/ec/acc/security_project_test.go b/ec/acc/security_project_test.go index 2519d5930..0e5cee9f3 100644 --- a/ec/acc/security_project_test.go +++ b/ec/acc/security_project_test.go @@ -81,17 +81,17 @@ func TestAccSecurityProject(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "credentials.username"), resource.TestCheckResourceAttrSet(resourceName, "credentials.password"), resource.TestCheckResourceAttrSet(resourceName, "cloud_id"), - ), - }, - { - // Test import. - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"credentials"}, + ), + }, + { + // Test import. + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"credentials"}, + }, }, - }, -}) + }) } func TestAccSecurityProjectWithAdminFeaturesAndProductTypes(t *testing.T) { From 829d086e9691b5db18830bc8fa542fce9ebaa4fc Mon Sep 17 00:00:00 2001 From: Gil Raphaelli Date: Tue, 6 Jan 2026 13:05:20 -0500 Subject: [PATCH 3/8] simplify import with ImportStatePassthroughID Co-authored-by: Dmitry Onishchenko <8962171+dimuon@users.noreply.github.com> --- .../projectresource/elasticsearch.go | 4 - ec/ecresource/projectresource/import.go | 26 +--- ec/ecresource/projectresource/mocks.gen.go | 14 -- .../projectresource/observability.go | 4 - ec/ecresource/projectresource/resource.go | 1 - .../projectresource/resource_test.go | 121 ++---------------- ec/ecresource/projectresource/security.go | 4 - 7 files changed, 10 insertions(+), 164 deletions(-) diff --git a/ec/ecresource/projectresource/elasticsearch.go b/ec/ecresource/projectresource/elasticsearch.go index 6fa634292..0c528adf1 100644 --- a/ec/ecresource/projectresource/elasticsearch.go +++ b/ec/ecresource/projectresource/elasticsearch.go @@ -57,10 +57,6 @@ func (es elasticsearchModelReader) GetID(model resource_elasticsearch_project.El return model.Id.ValueString() } -func (es elasticsearchModelReader) NewEmptyModel() resource_elasticsearch_project.ElasticsearchProjectModel { - return resource_elasticsearch_project.ElasticsearchProjectModel{} -} - func (es elasticsearchModelReader) Modify(plan resource_elasticsearch_project.ElasticsearchProjectModel, state resource_elasticsearch_project.ElasticsearchProjectModel, cfg resource_elasticsearch_project.ElasticsearchProjectModel) resource_elasticsearch_project.ElasticsearchProjectModel { plan.Credentials = useStateForUnknown(plan.Credentials, state.Credentials) plan.Endpoints = useStateForUnknown(plan.Endpoints, state.Endpoints) diff --git a/ec/ecresource/projectresource/import.go b/ec/ecresource/projectresource/import.go index b3333d2f8..5f714f9e5 100644 --- a/ec/ecresource/projectresource/import.go +++ b/ec/ecresource/projectresource/import.go @@ -20,32 +20,10 @@ package projectresource import ( "context" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" ) func (r *Resource[T]) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { - if !resourceReady(r, &response.Diagnostics) { - return - } - - projectID := request.ID - - // Create an empty model to populate with data from the API - emptyModel := r.modelHandler.NewEmptyModel() - - found, model, diags := r.api.Read(ctx, projectID, emptyModel) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - if !found { - response.Diagnostics.AddError( - "Resource not found", - "No project was found with the specified ID: "+projectID, - ) - return - } - - response.Diagnostics.Append(response.State.Set(ctx, model)...) + resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) } diff --git a/ec/ecresource/projectresource/mocks.gen.go b/ec/ecresource/projectresource/mocks.gen.go index f7ec8041f..f1430c7e5 100644 --- a/ec/ecresource/projectresource/mocks.gen.go +++ b/ec/ecresource/projectresource/mocks.gen.go @@ -151,20 +151,6 @@ func (mr *MockmodelHandlerMockRecorder[T]) Schema(arg0, arg1, arg2 any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Schema", reflect.TypeOf((*MockmodelHandler[T])(nil).Schema), arg0, arg1, arg2) } -// NewEmptyModel mocks base method. -func (m *MockmodelHandler[T]) NewEmptyModel() T { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewEmptyModel") - ret0, _ := ret[0].(T) - return ret0 -} - -// NewEmptyModel indicates an expected call of NewEmptyModel. -func (mr *MockmodelHandlerMockRecorder[T]) NewEmptyModel() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewEmptyModel", reflect.TypeOf((*MockmodelHandler[T])(nil).NewEmptyModel)) -} - // Mockapi is a mock of api interface. type Mockapi[TModel any] struct { ctrl *gomock.Controller diff --git a/ec/ecresource/projectresource/observability.go b/ec/ecresource/projectresource/observability.go index b0ac39cc4..d09369afe 100644 --- a/ec/ecresource/projectresource/observability.go +++ b/ec/ecresource/projectresource/observability.go @@ -55,10 +55,6 @@ func (obs observabilityModelReader) GetID(model resource_observability_project.O return model.Id.ValueString() } -func (obs observabilityModelReader) NewEmptyModel() resource_observability_project.ObservabilityProjectModel { - return resource_observability_project.ObservabilityProjectModel{} -} - func (obs observabilityModelReader) Modify(plan resource_observability_project.ObservabilityProjectModel, state resource_observability_project.ObservabilityProjectModel, cfg resource_observability_project.ObservabilityProjectModel) resource_observability_project.ObservabilityProjectModel { plan.Credentials = useStateForUnknown(plan.Credentials, state.Credentials) plan.Endpoints = useStateForUnknown(plan.Endpoints, state.Endpoints) diff --git a/ec/ecresource/projectresource/resource.go b/ec/ecresource/projectresource/resource.go index 86bcb1c81..2e4aa3066 100644 --- a/ec/ecresource/projectresource/resource.go +++ b/ec/ecresource/projectresource/resource.go @@ -53,7 +53,6 @@ type modelHandler[T any] interface { ReadFrom(context.Context, modelGetter) (*T, diag.Diagnostics) GetID(T) string Modify(T, T, T) T - NewEmptyModel() T } type api[TModel any] interface { diff --git a/ec/ecresource/projectresource/resource_test.go b/ec/ecresource/projectresource/resource_test.go index f7f9b01bb..9da98d201 100644 --- a/ec/ecresource/projectresource/resource_test.go +++ b/ec/ecresource/projectresource/resource_test.go @@ -25,7 +25,7 @@ import ( "github.com/elastic/terraform-provider-ec/ec/internal" "github.com/elastic/terraform-provider-ec/ec/internal/gen/serverless/mocks" "github.com/elastic/terraform-provider-ec/ec/internal/gen/serverless/resource_elasticsearch_project" - "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/elastic/terraform-provider-ec/ec/internal/util" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -185,136 +185,31 @@ func TestModifyPlan(t *testing.T) { } func TestImportState(t *testing.T) { - ctrl := gomock.NewController(t) - - t.Run("should error when api is not ready", func(t *testing.T) { - ctx := context.Background() - req := resource.ImportStateRequest{ - ID: "project-id", - } - res := resource.ImportStateResponse{ - State: tfsdk.State{ - Schema: resource_elasticsearch_project.ElasticsearchProjectResourceSchema(ctx), - }, - } - - mockApi := NewMockapi[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) - mockApi.EXPECT().Ready().Return(false) - - r := Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{ - api: mockApi, - } - r.ImportState(ctx, req, &res) - - require.True(t, res.Diagnostics.HasError()) - require.Contains(t, res.Diagnostics.Errors()[0].Summary(), "Unconfigured API Client") - }) - - t.Run("should error when project is not found", func(t *testing.T) { - ctx := context.Background() - projectID := "non-existent-project-id" - req := resource.ImportStateRequest{ - ID: projectID, - } - res := resource.ImportStateResponse{ - State: tfsdk.State{ - Schema: resource_elasticsearch_project.ElasticsearchProjectResourceSchema(ctx), - }, - } - - emptyModel := resource_elasticsearch_project.ElasticsearchProjectModel{} - - mockApi := NewMockapi[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) - mockApi.EXPECT().Ready().Return(true) - mockApi.EXPECT().Read(ctx, projectID, emptyModel).Return(false, emptyModel, nil) - - mockHandler := NewMockmodelHandler[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) - mockHandler.EXPECT().NewEmptyModel().Return(emptyModel) - - r := Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{ - api: mockApi, - modelHandler: mockHandler, - } - r.ImportState(ctx, req, &res) - - require.True(t, res.Diagnostics.HasError()) - require.Contains(t, res.Diagnostics.Errors()[0].Summary(), "Resource not found") - }) - - t.Run("should error when api read fails", func(t *testing.T) { + t.Run("should successfully import project", func(t *testing.T) { ctx := context.Background() projectID := "project-id" req := resource.ImportStateRequest{ ID: projectID, } - res := resource.ImportStateResponse{ - State: tfsdk.State{ - Schema: resource_elasticsearch_project.ElasticsearchProjectResourceSchema(ctx), - }, - } - + schema := resource_elasticsearch_project.ElasticsearchProjectResourceSchema(ctx) emptyModel := resource_elasticsearch_project.ElasticsearchProjectModel{} + emptyValue := util.TfTypesValueFromGoTypeValue(t, emptyModel, schema.Type()) - mockApi := NewMockapi[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) - mockApi.EXPECT().Ready().Return(true) - mockApi.EXPECT().Read(ctx, projectID, emptyModel).Return(false, emptyModel, diag.Diagnostics{ - diag.NewErrorDiagnostic("API Error", "Failed to read project"), - }) - - mockHandler := NewMockmodelHandler[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) - mockHandler.EXPECT().NewEmptyModel().Return(emptyModel) - - r := Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{ - api: mockApi, - modelHandler: mockHandler, - } - r.ImportState(ctx, req, &res) - - require.True(t, res.Diagnostics.HasError()) - require.Contains(t, res.Diagnostics.Errors()[0].Summary(), "API Error") - }) - - t.Run("should successfully import project", func(t *testing.T) { - ctx := context.Background() - projectID := "project-id" - req := resource.ImportStateRequest{ - ID: projectID, - } res := resource.ImportStateResponse{ State: tfsdk.State{ - Schema: resource_elasticsearch_project.ElasticsearchProjectResourceSchema(ctx), + Schema: schema, + Raw: emptyValue, }, } - emptyModel := resource_elasticsearch_project.ElasticsearchProjectModel{} - readModel := resource_elasticsearch_project.ElasticsearchProjectModel{ - Id: types.StringValue(projectID), - Name: types.StringValue("imported-project"), - RegionId: types.StringValue("us-east-1"), - } - - mockApi := NewMockapi[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) - mockApi.EXPECT().Ready().Return(true) - mockApi.EXPECT().Read(ctx, projectID, emptyModel).Return(true, readModel, nil) - - mockHandler := NewMockmodelHandler[resource_elasticsearch_project.ElasticsearchProjectModel](ctrl) - mockHandler.EXPECT().NewEmptyModel().Return(emptyModel) - - r := Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{ - api: mockApi, - modelHandler: mockHandler, - } + r := Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{} r.ImportState(ctx, req, &res) require.False(t, res.Diagnostics.HasError()) - // Validate that the imported values were set in the state + // Validate that the imported ID was set in the state var id string res.State.GetAttribute(ctx, path.Root("id"), &id) require.Equal(t, projectID, id) - - var name string - res.State.GetAttribute(ctx, path.Root("name"), &name) - require.Equal(t, "imported-project", name) }) } diff --git a/ec/ecresource/projectresource/security.go b/ec/ecresource/projectresource/security.go index ef17da9e3..2a9f60d51 100644 --- a/ec/ecresource/projectresource/security.go +++ b/ec/ecresource/projectresource/security.go @@ -74,10 +74,6 @@ func (sec securityModelReader) GetID(model resource_security_project.SecurityPro return model.Id.ValueString() } -func (sec securityModelReader) NewEmptyModel() resource_security_project.SecurityProjectModel { - return resource_security_project.SecurityProjectModel{} -} - func (sec securityModelReader) Modify(plan resource_security_project.SecurityProjectModel, state resource_security_project.SecurityProjectModel, cfg resource_security_project.SecurityProjectModel) resource_security_project.SecurityProjectModel { plan.Credentials = useStateForUnknown(plan.Credentials, state.Credentials) plan.Endpoints = useStateForUnknown(plan.Endpoints, state.Endpoints) From 1af7c3dfef667af6d1604be81a027d4e2850f57f Mon Sep 17 00:00:00 2001 From: Gil Raphaelli Date: Tue, 6 Jan 2026 13:11:40 -0500 Subject: [PATCH 4/8] satisfy tfproviderlint --- ec/acc/elasticsearch_project_test.go | 2 +- ec/acc/observability_project_test.go | 2 +- ec/acc/security_project_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ec/acc/elasticsearch_project_test.go b/ec/acc/elasticsearch_project_test.go index 8edb7108b..6dbb6d29b 100644 --- a/ec/acc/elasticsearch_project_test.go +++ b/ec/acc/elasticsearch_project_test.go @@ -113,7 +113,7 @@ resource ec_elasticsearch_project "%s" { `, id, name, region, alias) } -func TestAccElasticsearchProjectImport(t *testing.T) { +func TestAcc_ElasticsearchProjectImport(t *testing.T) { resId := "import_project" resourceName := fmt.Sprintf("ec_elasticsearch_project.%s", resId) randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) diff --git a/ec/acc/observability_project_test.go b/ec/acc/observability_project_test.go index 97ea55fff..9a9ab2546 100644 --- a/ec/acc/observability_project_test.go +++ b/ec/acc/observability_project_test.go @@ -186,7 +186,7 @@ resource ec_observability_project "%s" { `, id, name, region, productTier) } -func TestAccObservabilityProjectImport(t *testing.T) { +func TestAcc_ObservabilityProjectImport(t *testing.T) { resId := "import_project" resourceName := fmt.Sprintf("ec_observability_project.%s", resId) randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) diff --git a/ec/acc/security_project_test.go b/ec/acc/security_project_test.go index ebf760623..1062193c4 100644 --- a/ec/acc/security_project_test.go +++ b/ec/acc/security_project_test.go @@ -163,7 +163,7 @@ resource ec_security_project "%s" { `, id, name, region, adminPackage) } -func TestAccSecurityProjectImport(t *testing.T) { +func TestAcc_SecurityProjectImport(t *testing.T) { resId := "import_project" resourceName := fmt.Sprintf("ec_security_project.%s", resId) randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) From d7191018924979f4998d5ddceca70a9b119bf6c2 Mon Sep 17 00:00:00 2001 From: Gil Raphaelli Date: Tue, 6 Jan 2026 15:12:25 -0500 Subject: [PATCH 5/8] set obs product tier on import --- ec/ecresource/projectresource/observability.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ec/ecresource/projectresource/observability.go b/ec/ecresource/projectresource/observability.go index d09369afe..19e87c44d 100644 --- a/ec/ecresource/projectresource/observability.go +++ b/ec/ecresource/projectresource/observability.go @@ -277,6 +277,14 @@ func (obs observabilityApi) Read(ctx context.Context, id string, model resource_ model.RegionId = basetypes.NewStringValue(resp.JSON200.RegionId) model.Type = basetypes.NewStringValue(string(resp.JSON200.Type)) + // Set product_tier from API response, defaulting to "complete" if not present + if resp.JSON200.ProductTier != nil { + model.ProductTier = basetypes.NewStringValue(string(*resp.JSON200.ProductTier)) + } else { + // Default value as per schema + model.ProductTier = basetypes.NewStringValue(string(serverless.ObservabilityProjectProductTierComplete)) + } + return true, model, nil } From 21eed97f25ce9d3aeeaf4e8daa0a177af9d592db Mon Sep 17 00:00:00 2001 From: Gil Raphaelli Date: Tue, 6 Jan 2026 15:42:14 -0500 Subject: [PATCH 6/8] temp disable product type check for security project import --- ec/acc/security_project_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ec/acc/security_project_test.go b/ec/acc/security_project_test.go index 1062193c4..f36e0fbb9 100644 --- a/ec/acc/security_project_test.go +++ b/ec/acc/security_project_test.go @@ -85,10 +85,11 @@ func TestAcc_SecurityProject(t *testing.T) { }, { // Test import. - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"credentials"}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + // TODO(gr): reenable after checking the order of product_types is not important + ImportStateVerifyIgnore: []string{"credentials", "product_types"}, }, }, }) From 56a385446da42b56592009ec57e0bc9456f1c23d Mon Sep 17 00:00:00 2001 From: Gil Raphaelli Date: Tue, 6 Jan 2026 16:09:10 -0500 Subject: [PATCH 7/8] expect complete product tier by default --- .../projectresource/observability_test.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ec/ecresource/projectresource/observability_test.go b/ec/ecresource/projectresource/observability_test.go index 844bb18fb..7e03ab2aa 100644 --- a/ec/ecresource/projectresource/observability_test.go +++ b/ec/ecresource/projectresource/observability_test.go @@ -902,9 +902,10 @@ func TestObservabilityApi_Read(t *testing.T) { "suspended_reason": basetypes.NewStringNull(), }, ), - Name: types.StringValue(readModel.Name), - RegionId: types.StringValue(readModel.RegionId), - Type: types.StringValue(string(readModel.Type)), + Name: types.StringValue(readModel.Name), + RegionId: types.StringValue(readModel.RegionId), + Type: types.StringValue(string(readModel.Type)), + ProductTier: types.StringValue(string(serverless.ObservabilityProjectProductTierComplete)), } mockApiClient := mocks.NewMockClientWithResponsesInterface(ctrl) @@ -976,9 +977,10 @@ func TestObservabilityApi_Read(t *testing.T) { "suspended_reason": basetypes.NewStringValue(*readModel.Metadata.SuspendedReason), }, ), - Name: types.StringValue(readModel.Name), - RegionId: types.StringValue(readModel.RegionId), - Type: types.StringValue(string(readModel.Type)), + Name: types.StringValue(readModel.Name), + RegionId: types.StringValue(readModel.RegionId), + Type: types.StringValue(string(readModel.Type)), + ProductTier: types.StringValue(string(serverless.ObservabilityProjectProductTierComplete)), } mockApiClient := mocks.NewMockClientWithResponsesInterface(ctrl) From a214fe4b96f48d26293f180ee5e0a3bce4a31f98 Mon Sep 17 00:00:00 2001 From: Gil Raphaelli Date: Wed, 7 Jan 2026 10:30:03 -0500 Subject: [PATCH 8/8] rely on empty plan check to validate product types import Co-authored-by: Toby Brain --- ec/acc/security_project_test.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ec/acc/security_project_test.go b/ec/acc/security_project_test.go index f36e0fbb9..5c060040d 100644 --- a/ec/acc/security_project_test.go +++ b/ec/acc/security_project_test.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -88,8 +89,17 @@ func TestAcc_SecurityProject(t *testing.T) { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, - // TODO(gr): reenable after checking the order of product_types is not important + // product_types are verified by ImportPlanChecks, so can be ignored here. ImportStateVerifyIgnore: []string{"credentials", "product_types"}, + // Use ImportPlanChecks to verify semantic equality of product_types. + // ExpectEmptyPlan confirms that the plan after import shows no changes, + // which indicates semantic equality is working correctly even if product_types + // are returned in a different order by the API. + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, }, }, })