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 830073037..6dbb6d29b 100644 --- a/ec/acc/elasticsearch_project_test.go +++ b/ec/acc/elasticsearch_project_test.go @@ -83,6 +83,13 @@ func TestAcc_ElasticsearchProject(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "cloud_id"), ), }, + { + // Test import. + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"credentials"}, + }, }, }) } @@ -106,6 +113,52 @@ resource ec_elasticsearch_project "%s" { `, id, name, region, alias) } +func TestAcc_ElasticsearchProjectImport(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 cad8e5cbb..9a9ab2546 100644 --- a/ec/acc/observability_project_test.go +++ b/ec/acc/observability_project_test.go @@ -91,6 +91,13 @@ func TestAcc_ObservabilityProject(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 TestAcc_ObservabilityProjectImport(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 06273e14e..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" ) @@ -83,6 +84,23 @@ func TestAcc_SecurityProject(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "cloud_id"), ), }, + { + // Test import. + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + // 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(), + }, + }, + }, }, }) } @@ -156,6 +174,52 @@ resource ec_security_project "%s" { `, id, name, region, adminPackage) } +func TestAcc_SecurityProjectImport(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/import.go b/ec/ecresource/projectresource/import.go new file mode 100644 index 000000000..5f714f9e5 --- /dev/null +++ b/ec/ecresource/projectresource/import.go @@ -0,0 +1,29 @@ +// 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/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *Resource[T]) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) +} 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 } 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) diff --git a/ec/ecresource/projectresource/resource.go b/ec/ecresource/projectresource/resource.go index 0288eb4e6..2e4aa3066 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] diff --git a/ec/ecresource/projectresource/resource_test.go b/ec/ecresource/projectresource/resource_test.go index d69d9c0ee..9da98d201 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/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" @@ -182,3 +183,33 @@ func TestModifyPlan(t *testing.T) { require.Equal(t, planModel.Id.ValueString(), id) }) } + +func TestImportState(t *testing.T) { + t.Run("should successfully import project", func(t *testing.T) { + ctx := context.Background() + projectID := "project-id" + req := resource.ImportStateRequest{ + ID: projectID, + } + schema := resource_elasticsearch_project.ElasticsearchProjectResourceSchema(ctx) + emptyModel := resource_elasticsearch_project.ElasticsearchProjectModel{} + emptyValue := util.TfTypesValueFromGoTypeValue(t, emptyModel, schema.Type()) + + res := resource.ImportStateResponse{ + State: tfsdk.State{ + Schema: schema, + Raw: emptyValue, + }, + } + + r := Resource[resource_elasticsearch_project.ElasticsearchProjectModel]{} + r.ImportState(ctx, req, &res) + + require.False(t, res.Diagnostics.HasError()) + + // 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) + }) +} 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.