diff --git a/ec/acc/deployment_extension_basic_test.go b/ec/acc/deployment_extension_basic_test.go index 7696d6a97..c0fdfc5a0 100644 --- a/ec/acc/deployment_extension_basic_test.go +++ b/ec/acc/deployment_extension_basic_test.go @@ -65,6 +65,46 @@ func TestAccDeploymentExtension_basic(t *testing.T) { }) } +func TestAccDeploymentExtension_UpgradeFrom0_4_1(t *testing.T) { + resName := "ec_deployment_extension.my_extension" + randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + cfg := fixtureAccExtensionBasicWithTF(t, "testdata/extension_basic.tf", randomName, "desc") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccDeploymentTrafficFilterDestroy, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "ec": { + VersionConstraint: "0.4.1", + Source: "elastic/ec", + }, + }, + Config: cfg, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resName, "name", randomName), + resource.TestCheckResourceAttr(resName, "version", "*"), + resource.TestCheckResourceAttr(resName, "description", "desc"), + resource.TestCheckResourceAttr(resName, "extension_type", "bundle"), + ), + }, + { + PlanOnly: true, + ProtoV6ProviderFactories: testAccProviderFactory, + Config: cfg, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resName, "name", randomName), + resource.TestCheckResourceAttr(resName, "version", "*"), + resource.TestCheckResourceAttr(resName, "description", "desc"), + resource.TestCheckResourceAttr(resName, "extension_type", "bundle"), + ), + }, + }, + }) +} + func fixtureAccExtensionBasicWithTF(t *testing.T, tfFileName, extensionName, description string) string { t.Helper() diff --git a/ec/ecdatasource/deploymentsdatasource/expanders.go b/ec/ecdatasource/deploymentsdatasource/expanders.go index 635883ab8..7f21c46e7 100644 --- a/ec/ecdatasource/deploymentsdatasource/expanders.go +++ b/ec/ecdatasource/deploymentsdatasource/expanders.go @@ -40,7 +40,7 @@ func expandFilters(ctx context.Context, state modelV0) (*models.SearchRequest, d Prefix: map[string]models.PrefixQuery{ // The "keyword" addition denotes that the query will be using a keyword // field rather than a text field in order to ensure the query is not analyzed - "name.keyword": {Value: ec.String(namePrefix)}, + "name.keyword": {Value: &namePrefix}, }, }) } @@ -180,11 +180,11 @@ func expandResourceFilters(ctx context.Context, resources *types.List, resourceK func newNestedTermQuery(path, term string, value string) *models.QueryContainer { return &models.QueryContainer{ Nested: &models.NestedQuery{ - Path: ec.String(path), + Path: &path, Query: &models.QueryContainer{ Term: map[string]models.TermQuery{ term: { - Value: ec.String(value), + Value: &value, }, }, }, diff --git a/ec/ecresource/elasticsearchkeystoreresource/create.go b/ec/ecresource/elasticsearchkeystoreresource/create.go index e65e58356..dcca8d4ce 100644 --- a/ec/ecresource/elasticsearchkeystoreresource/create.go +++ b/ec/ecresource/elasticsearchkeystoreresource/create.go @@ -57,12 +57,12 @@ func (r Resource) Create(ctx context.Context, request resource.CreateRequest, re found, diags := r.read(ctx, newState.DeploymentID.Value, &newState) response.Diagnostics.Append(diags...) if !found { - // We can't unset the state here, and must make sure to set the state according to the plan below. - // So all we do is add a warning. - diags.AddWarning( - "Failed to read Elasticsearch keystore.", - "Please run terraform refresh to ensure a consistent state.", + response.Diagnostics.AddError( + "Failed to read Elasticsearch keystore after create.", + "Failed to read Elasticsearch keystore after create.", ) + response.State.RemoveResource(ctx) + return } if response.Diagnostics.HasError() { return diff --git a/ec/ecresource/elasticsearchkeystoreresource/expanders.go b/ec/ecresource/elasticsearchkeystoreresource/expanders.go index e5a9a766b..009924796 100644 --- a/ec/ecresource/elasticsearchkeystoreresource/expanders.go +++ b/ec/ecresource/elasticsearchkeystoreresource/expanders.go @@ -22,7 +22,6 @@ import ( "encoding/json" "github.com/elastic/cloud-sdk-go/pkg/models" - "github.com/elastic/cloud-sdk-go/pkg/util/ec" ) func expandModel(ctx context.Context, state modelV0) *models.KeystoreContents { @@ -39,7 +38,7 @@ func expandModel(ctx context.Context, state modelV0) *models.KeystoreContents { return &models.KeystoreContents{ Secrets: map[string]models.KeystoreSecret{ secretName: { - AsFile: ec.Bool(state.AsFile.Value), + AsFile: &state.AsFile.Value, Value: value, }, }, diff --git a/ec/ecresource/elasticsearchkeystoreresource/resource_test.go b/ec/ecresource/elasticsearchkeystoreresource/resource_test.go index 0139913d2..4100b64b4 100644 --- a/ec/ecresource/elasticsearchkeystoreresource/resource_test.go +++ b/ec/ecresource/elasticsearchkeystoreresource/resource_test.go @@ -125,9 +125,9 @@ func TestResourceElasticsearchKeyStore_notFoundAfterCreate_and_gracefulDeletion( ), Steps: []r.TestStep{ { // Create resource - Config: externalKeystore1, - Check: checkResource1(), - ExpectNonEmptyPlan: true, + Config: externalKeystore1, + Check: checkResource1(), + ExpectError: regexp.MustCompile(`Failed to read Elasticsearch keystore after create.`), }, }, }) diff --git a/ec/ecresource/elasticsearchkeystoreresource/update.go b/ec/ecresource/elasticsearchkeystoreresource/update.go index 153ab92a3..303d075d3 100644 --- a/ec/ecresource/elasticsearchkeystoreresource/update.go +++ b/ec/ecresource/elasticsearchkeystoreresource/update.go @@ -54,12 +54,12 @@ func (r Resource) Update(ctx context.Context, request resource.UpdateRequest, re return } if !found { - // We can't unset the state here, and must make sure to set the state according to the plan below. - // So all we do is add a warning. - diags.AddWarning( - "Failed to read Elasticsearch keystore.", - "Please run terraform refresh to ensure a consistent state.", + response.Diagnostics.AddError( + "Failed to read Elasticsearch keystore after update.", + "Failed to read Elasticsearch keystore after update.", ) + response.State.RemoveResource(ctx) + return } // Finally, set the state diff --git a/ec/ecresource/extensionresource/create.go b/ec/ecresource/extensionresource/create.go index 159a47062..980d16132 100644 --- a/ec/ecresource/extensionresource/create.go +++ b/ec/ecresource/extensionresource/create.go @@ -1,73 +1,65 @@ -// 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 extensionresource import ( "context" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/elastic/cloud-sdk-go/pkg/api" "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/extensionapi" - "github.com/elastic/cloud-sdk-go/pkg/models" - "github.com/elastic/cloud-sdk-go/pkg/multierror" ) -// createResource will create a new deployment extension -func createResource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*api.API) +func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + if !resourceReady(r, &response.Diagnostics) { + return + } + + var newState modelV0 + + diags := request.Plan.Get(ctx, &newState) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } - model, err := createRequest(client, d) + model, err := extensionapi.Create( + extensionapi.CreateParams{ + API: r.client, + Name: newState.Name.Value, + Version: newState.Version.Value, + Type: newState.ExtensionType.Value, + Description: newState.Description.Value, + DownloadURL: newState.DownloadURL.Value, + }, + ) if err != nil { - return diag.FromErr(err) + response.Diagnostics.AddError(err.Error(), err.Error()) + return } - d.SetId(*model.ID) + newState.ID = types.String{Value: *model.ID} - if _, ok := d.GetOk("file_path"); ok { - if err := uploadExtension(client, d); err != nil { - return diag.FromErr(multierror.NewPrefixed("failed to upload file", err)) + if !newState.FilePath.IsNull() && newState.FilePath.Value != "" { + response.Diagnostics.Append(r.uploadExtension(newState)...) + if response.Diagnostics.HasError() { + return } } - return readResource(ctx, d, meta) -} - -func createRequest(client *api.API, d *schema.ResourceData) (*models.Extension, error) { - name := d.Get("name").(string) - version := d.Get("version").(string) - extensionType := d.Get("extension_type").(string) - description := d.Get("description").(string) - downloadURL := d.Get("download_url").(string) - body := extensionapi.CreateParams{ - API: client, - Name: name, - Version: version, - Type: extensionType, - Description: description, - DownloadURL: downloadURL, + found, diags := r.read(newState.ID.Value, &newState) + response.Diagnostics.Append(diags...) + if !found { + response.Diagnostics.AddError( + "Failed to read deployment extension after create.", + "Failed to read deployment extension after create.", + ) + response.State.RemoveResource(ctx) + return } - - res, err := extensionapi.Create(body) - if err != nil { - return nil, err + if response.Diagnostics.HasError() { + return } - return res, nil + // Finally, set the state + response.Diagnostics.Append(response.State.Set(ctx, newState)...) } diff --git a/ec/ecresource/extensionresource/create_test.go b/ec/ecresource/extensionresource/create_test.go deleted file mode 100644 index 78e527871..000000000 --- a/ec/ecresource/extensionresource/create_test.go +++ /dev/null @@ -1,94 +0,0 @@ -// 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 extensionresource - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/elastic/cloud-sdk-go/pkg/api" - "github.com/elastic/cloud-sdk-go/pkg/api/mock" - - "github.com/elastic/terraform-provider-ec/ec/internal/util" -) - -func Test_createResource(t *testing.T) { - tc500Err := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - wantTC500 := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - - type args struct { - ctx context.Context - d *schema.ResourceData - meta interface{} - } - tests := []struct { - name string - args args - want diag.Diagnostics - wantRD *schema.ResourceData - }{ - { - name: "returns an error when it receives a 500", - args: args{ - d: tc500Err, - meta: api.NewMock(mock.NewErrorResponse(500, mock.APIError{ - Code: "some", Message: "message", - })), - }, - want: diag.Diagnostics{ - { - Severity: diag.Error, - Summary: "api error: 1 error occurred:\n\t* some: message\n\n", - }, - }, - wantRD: wantTC500, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := createResource(tt.args.ctx, tt.args.d, tt.args.meta) - assert.Equal(t, tt.want, got) - var want interface{} - if tt.wantRD != nil { - if s := tt.wantRD.State(); s != nil { - want = s.Attributes - } - } - - var gotState interface{} - if s := tt.args.d.State(); s != nil { - gotState = s.Attributes - } - - assert.Equal(t, want, gotState) - }) - } -} diff --git a/ec/ecresource/extensionresource/delete.go b/ec/ecresource/extensionresource/delete.go index 07152fa9c..a34620769 100644 --- a/ec/ecresource/extensionresource/delete.go +++ b/ec/ecresource/extensionresource/delete.go @@ -1,51 +1,36 @@ -// 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 extensionresource import ( "context" "errors" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/elastic/cloud-sdk-go/pkg/api" "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/extensionapi" "github.com/elastic/cloud-sdk-go/pkg/client/extensions" ) -func deleteResource(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*api.API) +func (r *Resource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + if !resourceReady(r, &response.Diagnostics) { + return + } + + var state modelV0 + + diags := request.State.Get(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } if err := extensionapi.Delete(extensionapi.DeleteParams{ - API: client, - ExtensionID: d.Id(), + API: r.client, + ExtensionID: state.ID.Value, }); err != nil { - if alreadyDestroyed(err) { - d.SetId("") - return nil + if !alreadyDestroyed(err) { + response.Diagnostics.AddError(err.Error(), err.Error()) } - - return diag.FromErr(err) } - - d.SetId("") - return nil } func alreadyDestroyed(err error) bool { diff --git a/ec/ecresource/extensionresource/delete_test.go b/ec/ecresource/extensionresource/delete_test.go deleted file mode 100644 index 3e5c615b4..000000000 --- a/ec/ecresource/extensionresource/delete_test.go +++ /dev/null @@ -1,138 +0,0 @@ -// 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 extensionresource - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/elastic/cloud-sdk-go/pkg/api" - "github.com/elastic/cloud-sdk-go/pkg/api/mock" - - "github.com/elastic/terraform-provider-ec/ec/internal/util" -) - -func Test_deleteResource(t *testing.T) { - tc200 := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - wantTC200 := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - wantTC200.SetId("") - - tc500Err := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - wantTC500 := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - - tc404Err := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - wantTC404 := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - wantTC404.SetId("") - - type args struct { - ctx context.Context - d *schema.ResourceData - meta interface{} - } - tests := []struct { - name string - args args - want diag.Diagnostics - wantRD *schema.ResourceData - }{ - { - name: "returns nil when it receives a 200", - args: args{ - d: tc200, - meta: api.NewMock(mock.New200Response(nil)), - }, - want: nil, - wantRD: wantTC200, - }, - { - name: "returns an error when it receives a 500", - args: args{ - d: tc500Err, - meta: api.NewMock(mock.NewErrorResponse(500, mock.APIError{ - Code: "some", Message: "message", - })), - }, - want: diag.Diagnostics{ - { - Severity: diag.Error, - Summary: "api error: 1 error occurred:\n\t* some: message\n\n", - }, - }, - wantRD: wantTC500, - }, - { - name: "returns nil and unsets the state when the error is known", - args: args{ - d: tc404Err, - meta: api.NewMock(mock.NewErrorResponse(404, mock.APIError{ - Code: "some", Message: "message", - })), - }, - want: nil, - wantRD: wantTC404, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := deleteResource(tt.args.ctx, tt.args.d, tt.args.meta) - assert.Equal(t, tt.want, got) - var want interface{} - if tt.wantRD != nil { - if s := tt.wantRD.State(); s != nil { - want = s.Attributes - } - } - - var gotState interface{} - if s := tt.args.d.State(); s != nil { - gotState = s.Attributes - } - - assert.Equal(t, want, gotState) - }) - } -} diff --git a/ec/ecresource/extensionresource/read.go b/ec/ecresource/extensionresource/read.go index 05624fc4f..b7372c725 100644 --- a/ec/ecresource/extensionresource/read.go +++ b/ec/ecresource/extensionresource/read.go @@ -1,57 +1,60 @@ -// 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 extensionresource import ( "context" "errors" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/elastic/cloud-sdk-go/pkg/api" "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/extensionapi" "github.com/elastic/cloud-sdk-go/pkg/client/extensions" "github.com/elastic/cloud-sdk-go/pkg/models" - "github.com/elastic/cloud-sdk-go/pkg/multierror" ) -func readResource(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*api.API) +func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + if !resourceReady(r, &response.Diagnostics) { + return + } + + var newState modelV0 + + diags := request.State.Get(ctx, &newState) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + found, diags := r.read(newState.ID.Value, &newState) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + if !found { + response.State.RemoveResource(ctx) + return + } + + // Finally, set the state + response.Diagnostics.Append(response.State.Set(ctx, newState)...) +} +func (r *Resource) read(id string, state *modelV0) (found bool, diags diag.Diagnostics) { res, err := extensionapi.Get(extensionapi.GetParams{ - API: client, - ExtensionID: d.Id(), + API: r.client, + ExtensionID: id, }) if err != nil { if extensionNotFound(err) { - d.SetId("") - return nil + return false, diags } - - return diag.FromErr(multierror.NewPrefixed("failed reading extension", err)) - } - - if err := modelToState(d, res); err != nil { - return diag.FromErr(err) + diags.AddError("failed reading extension", err.Error()) + return true, diags } - return nil + modelToState(res, state) + return true, diags } func extensionNotFound(err error) bool { @@ -61,40 +64,40 @@ func extensionNotFound(err error) bool { return errors.As(err, &extensionNotFound) } -func modelToState(d *schema.ResourceData, model *models.Extension) error { - if err := d.Set("name", model.Name); err != nil { - return err +func modelToState(model *models.Extension, state *modelV0) { + if model.Name != nil { + state.Name = types.String{Value: *model.Name} + } else { + state.Name = types.String{Null: true} } - if err := d.Set("version", model.Version); err != nil { - return err + if model.Version != nil { + state.Version = types.String{Value: *model.Version} + } else { + state.Version = types.String{Null: true} } - if err := d.Set("extension_type", model.ExtensionType); err != nil { - return err + if model.ExtensionType != nil { + state.ExtensionType = types.String{Value: *model.ExtensionType} + } else { + state.ExtensionType = types.String{Null: true} } - if err := d.Set("description", model.Description); err != nil { - return err - } + state.Description = types.String{Value: model.Description} - if err := d.Set("url", model.URL); err != nil { - return err + if model.URL != nil { + state.URL = types.String{Value: *model.URL} + } else { + state.URL = types.String{Null: true} } - if err := d.Set("download_url", model.DownloadURL); err != nil { - return err - } + state.DownloadURL = types.String{Value: model.DownloadURL} - if filemeta := model.FileMetadata; filemeta != nil { - if err := d.Set("last_modified", filemeta.LastModifiedDate.String()); err != nil { - return err - } - - if err := d.Set("size", filemeta.Size); err != nil { - return err - } + if metadata := model.FileMetadata; metadata != nil { + state.LastModified = types.String{Value: metadata.LastModifiedDate.String()} + state.Size = types.Int64{Value: metadata.Size} + } else { + state.LastModified = types.String{Null: true} + state.Size = types.Int64{Null: true} } - - return nil } diff --git a/ec/ecresource/extensionresource/read_test.go b/ec/ecresource/extensionresource/read_test.go deleted file mode 100644 index 827518986..000000000 --- a/ec/ecresource/extensionresource/read_test.go +++ /dev/null @@ -1,152 +0,0 @@ -// 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 extensionresource - -import ( - "context" - "testing" - - "github.com/go-openapi/strfmt" - "github.com/stretchr/testify/assert" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/elastic/cloud-sdk-go/pkg/api" - "github.com/elastic/cloud-sdk-go/pkg/api/mock" - "github.com/elastic/cloud-sdk-go/pkg/models" - "github.com/elastic/cloud-sdk-go/pkg/util/ec" - - "github.com/elastic/terraform-provider-ec/ec/internal/util" -) - -func Test_readResource(t *testing.T) { - tc200 := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - wantTC200 := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - - tc500Err := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - wantTC500 := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - - tc404Err := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - wantTC404 := util.NewResourceData(t, util.ResDataParams{ - ID: mock.ValidClusterID, - State: newExtension(), - Schema: newSchema(), - }) - wantTC404.SetId("") - - lastModified, _ := strfmt.ParseDateTime("2021-01-07T22:13:42.999Z") - type args struct { - ctx context.Context - d *schema.ResourceData - meta interface{} - } - tests := []struct { - name string - args args - want diag.Diagnostics - wantRD *schema.ResourceData - }{ - { - name: "returns nil when it receives a 200", - args: args{ - d: tc200, - meta: api.NewMock(mock.New200StructResponse(models.Extension{ - Name: ec.String("my_extension"), - ExtensionType: ec.String("bundle"), - Description: "my description", - Version: ec.String("*"), - DownloadURL: "https://example.com", - URL: ec.String("repo://1234"), - FileMetadata: &models.ExtensionFileMetadata{ - LastModifiedDate: lastModified, - Size: 1000, - }, - })), - }, - want: nil, - wantRD: wantTC200, - }, - { - name: "returns an error when it receives a 500", - args: args{ - d: tc500Err, - meta: api.NewMock(mock.NewErrorResponse(500, mock.APIError{ - Code: "some", Message: "message", - })), - }, - want: diag.Diagnostics{ - { - Severity: diag.Error, - Summary: "failed reading extension: 1 error occurred:\n\t* api error: some: message\n\n", - }, - }, - wantRD: wantTC500, - }, - { - name: "returns nil and unsets the state when the error is known", - args: args{ - d: tc404Err, - meta: api.NewMock(mock.NewErrorResponse(404, mock.APIError{ - Code: "some", Message: "message", - })), - }, - want: nil, - wantRD: wantTC404, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := readResource(tt.args.ctx, tt.args.d, tt.args.meta) - assert.Equal(t, tt.want, got) - var want interface{} - if tt.wantRD != nil { - if s := tt.wantRD.State(); s != nil { - want = s.Attributes - } - } - - var gotState interface{} - if s := tt.args.d.State(); s != nil { - gotState = s.Attributes - } - - assert.Equal(t, want, gotState) - }) - } -} diff --git a/ec/ecresource/extensionresource/resource.go b/ec/ecresource/extensionresource/resource.go deleted file mode 100644 index 42f7414a4..000000000 --- a/ec/ecresource/extensionresource/resource.go +++ /dev/null @@ -1,45 +0,0 @@ -// 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 extensionresource - -import ( - "time" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -// Resource returns the ec_deployment_extension resource schema. -func Resource() *schema.Resource { - return &schema.Resource{ - Description: "Elastic Cloud extension (plugin or bundle) to enhance the core functionality of Elasticsearch. Before you install an extension, be sure to check out the supported and official Elasticsearch plugins already available", - Schema: newSchema(), - - CreateContext: createResource, - ReadContext: readResource, - UpdateContext: updateResource, - DeleteContext: deleteResource, - - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - - Timeouts: &schema.ResourceTimeout{ - Default: schema.DefaultTimeout(10 * time.Minute), - }, - } -} diff --git a/ec/ecresource/extensionresource/resource_test.go b/ec/ecresource/extensionresource/resource_test.go new file mode 100644 index 000000000..9a4cb8b1a --- /dev/null +++ b/ec/ecresource/extensionresource/resource_test.go @@ -0,0 +1,416 @@ +// 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 extensionresource_test + +import ( + "net/http" + "net/url" + "regexp" + "testing" + + "github.com/go-openapi/strfmt" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/util/ec" + + provider "github.com/elastic/terraform-provider-ec/ec" +) + +func TestResourceDeploymentExtension(t *testing.T) { + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: protoV6ProviderFactoriesWithMockClient( + api.NewMock( + createResponse(), + readResponse1(), + readResponse1(), + readResponse1(), + updateResponse(), + + // Not testing for assertion as the content type is multipart/form-data + // with a boundary that is a randomly generated string which changes every time. + mock.Response{ + Response: http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: mock.NewStringBody("{}"), + }, + }, + + readResponse2(), + readResponse2(), + readResponse2(), + deleteResponse(), + ), + ), + Steps: []r.TestStep{ + { // Create resource + Config: deploymentExtension1, + Check: checkResource1(), + }, + { // Update resource + Config: deploymentExtension2, + Check: checkResource2(), + }, + { // Delete resource + Destroy: true, + Config: deploymentExtension2, + }, + }, + }) +} + +func TestResourceDeploymentExtension_failedCreate(t *testing.T) { + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: protoV6ProviderFactoriesWithMockClient( + api.NewMock( + mock.New500Response(mock.SampleInternalError().Response.Body), + ), + ), + Steps: []r.TestStep{ + { // Create resource + Config: deploymentExtension1, + ExpectError: regexp.MustCompile(`internal.server.error: There was an internal server error`), + }, + }, + }) +} + +func TestResourceDeploymentExtension_failedReadAfterCreate(t *testing.T) { + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: protoV6ProviderFactoriesWithMockClient( + api.NewMock( + createResponse(), + mock.New500Response(mock.SampleInternalError().Response.Body), + ), + ), + Steps: []r.TestStep{ + { // Create resource + Config: deploymentExtension1, + ExpectError: regexp.MustCompile(`internal.server.error: There was an internal server error`), + }, + }, + }) +} + +func TestResourceDeploymentExtension_notFoundAfterCreate(t *testing.T) { + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: protoV6ProviderFactoriesWithMockClient( + api.NewMock( + createResponse(), + mock.New404Response(mock.NewStringBody(`{ }`)), + ), + ), + Steps: []r.TestStep{ + { // Create resource + Config: deploymentExtension1, + ExpectError: regexp.MustCompile(`Failed to read deployment extension after create.`), + }, + }, + }) +} + +func TestResourceDeploymentExtension_failedUpdate(t *testing.T) { + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: protoV6ProviderFactoriesWithMockClient( + api.NewMock( + createResponse(), + readResponse1(), + readResponse1(), + readResponse1(), + mock.New500Response(mock.SampleInternalError().Response.Body), + deleteResponse(), + ), + ), + Steps: []r.TestStep{ + { // Create resource + Config: deploymentExtension1, + Check: checkResource1(), + }, + { // Update resource + Config: deploymentExtension2, + Check: checkResource2(), + ExpectError: regexp.MustCompile(`internal.server.error: There was an internal server error`), + }, + }, + }) +} + +func TestResourceDeploymentExtension_notFoundAfterUpdate(t *testing.T) { + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: protoV6ProviderFactoriesWithMockClient( + api.NewMock( + createResponse(), + readResponse1(), + readResponse1(), + readResponse1(), + updateResponse(), + mock.Response{ + Response: http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: mock.NewStringBody("{}"), + }, + }, + mock.New404Response(mock.NewStringBody(`{ }`)), + deleteResponse(), + ), + ), + Steps: []r.TestStep{ + { // Create resource + Config: deploymentExtension1, + Check: checkResource1(), + }, + { // Update resource + Config: deploymentExtension2, + Check: checkResource2(), + ExpectError: regexp.MustCompile(`Failed to read deployment extension after update.`), + }, + }, + }) +} + +func TestResourceDeploymentExtension_failedDelete(t *testing.T) { + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: protoV6ProviderFactoriesWithMockClient( + api.NewMock( + createResponse(), + readResponse1(), + readResponse1(), + readResponse1(), + mock.New500Response(mock.SampleInternalError().Response.Body), + deleteResponse(), + ), + ), + Steps: []r.TestStep{ + { // Create resource + Config: deploymentExtension1, + Check: checkResource1(), + }, + { // Delete resource + Destroy: true, + Config: deploymentExtension2, + ExpectError: regexp.MustCompile(`internal.server.error: There was an internal server error`), + }, + }, + }) +} + +func TestResourceDeploymentExtension_gracefulDeletion(t *testing.T) { + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: protoV6ProviderFactoriesWithMockClient( + api.NewMock( + createResponse(), + readResponse1(), + readResponse1(), + readResponse1(), + mock.New404ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultReadMockHeaders, + Method: "DELETE", + Host: api.DefaultMockHost, + Path: "/api/v1/deployments/extensions/someid", + }, + mock.NewStructBody(models.Extension{ + ID: ec.String("{ }"), + }, + ), + ), + ), + ), + Steps: []r.TestStep{ + { // Create resource + Config: deploymentExtension1, + Check: checkResource1(), + }, + { // Delete resource + Destroy: true, + Config: deploymentExtension2, + }, + }, + }) +} + +const deploymentExtension1 = ` +resource "ec_deployment_extension" "my_extension" { + name = "My extension" + description = "Some description" + version = "*" + extension_type = "bundle" +} +` +const deploymentExtension2 = ` +resource "ec_deployment_extension" "my_extension" { + name = "My updated extension" + description = "Some updated description" + version = "7.10.1" + extension_type = "bundle" + download_url = "https://example.com" + file_path = "testdata/test_extension_bundle.json" + file_hash = "abcd" +} +` + +func checkResource1() r.TestCheckFunc { + resource := "ec_deployment_extension.my_extension" + return r.ComposeAggregateTestCheckFunc( + r.TestCheckResourceAttr(resource, "id", "someid"), + r.TestCheckResourceAttr(resource, "name", "My extension"), + r.TestCheckResourceAttr(resource, "description", "Some description"), + r.TestCheckResourceAttr(resource, "version", "*"), + r.TestCheckResourceAttr(resource, "extension_type", "bundle"), + ) +} + +func checkResource2() r.TestCheckFunc { + resource := "ec_deployment_extension.my_extension" + return r.ComposeAggregateTestCheckFunc( + r.TestCheckResourceAttr(resource, "id", "someid"), + r.TestCheckResourceAttr(resource, "name", "My updated extension"), + r.TestCheckResourceAttr(resource, "description", "Some updated description"), + r.TestCheckResourceAttr(resource, "version", "7.10.1"), + r.TestCheckResourceAttr(resource, "extension_type", "bundle"), + r.TestCheckResourceAttr(resource, "download_url", "https://example.com"), + r.TestCheckResourceAttr(resource, "url", "repo://1234"), + r.TestCheckResourceAttr(resource, "last_modified", "2021-01-07T22:13:42.999Z"), + r.TestCheckResourceAttr(resource, "size", "1000"), + r.TestCheckResourceAttr(resource, "file_path", "testdata/test_extension_bundle.json"), + r.TestCheckResourceAttr(resource, "file_hash", "abcd"), + ) +} + +func createResponse() mock.Response { + return mock.New201ResponseAssertion( + &mock.RequestAssertion{ + Host: api.DefaultMockHost, + Header: api.DefaultWriteMockHeaders, + Method: "POST", + Path: "/api/v1/deployments/extensions", + Query: url.Values{}, + Body: mock.NewStringBody(`{"description":"Some description","extension_type":"bundle","name":"My extension","version":"*"}` + "\n"), + }, + mock.NewStringBody(`{"deployments":null,"description":"Some description","download_url":null,"extension_type":"bundle","id":"someid","name":"My extension","url":null,"version":"*"}`), + ) +} + +func updateResponse() mock.Response { + return mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Host: api.DefaultMockHost, + Header: api.DefaultWriteMockHeaders, + Method: "POST", + Path: "/api/v1/deployments/extensions/someid", + Query: url.Values{}, + Body: mock.NewStringBody(`{"description":"Some updated description","download_url":"https://example.com","extension_type":"bundle","name":"My updated extension","version":"7.10.1"}` + "\n"), + }, + mock.NewStructBody(models.Extension{ + ID: ec.String("someid"), + Name: ec.String("My updated extension"), + Description: "Some updated description", + ExtensionType: ec.String("bundle"), + Version: ec.String("7.10.1"), + DownloadURL: "https://example.com", + URL: ec.String("repo://1234"), + FileMetadata: &models.ExtensionFileMetadata{ + LastModifiedDate: lastModified(), + Size: 1000, + }, + })) +} + +func deleteResponse() mock.Response { + return mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultReadMockHeaders, + Method: "DELETE", + Host: api.DefaultMockHost, + Path: "/api/v1/deployments/extensions/someid", + }, + mock.NewStructBody(models.Extension{ + ID: ec.String("someid"), + }), + ) +} + +func readResponse1() mock.Response { + return mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultReadMockHeaders, + Method: "GET", + Host: api.DefaultMockHost, + Path: "/api/v1/deployments/extensions/someid", + Query: url.Values{"include_deployments": {"false"}}, + }, + mock.NewStructBody(models.Extension{ + ID: ec.String("someid"), + Name: ec.String("My extension"), + Description: "Some description", + ExtensionType: ec.String("bundle"), + Version: ec.String("*"), + }), + ) +} +func readResponse2() mock.Response { + return mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultReadMockHeaders, + Method: "GET", + Host: api.DefaultMockHost, + Path: "/api/v1/deployments/extensions/someid", + Query: url.Values{"include_deployments": {"false"}}, + }, + mock.NewStructBody(models.Extension{ + ID: ec.String("someid"), + Name: ec.String("My updated extension"), + Description: "Some updated description", + ExtensionType: ec.String("bundle"), + Version: ec.String("7.10.1"), + DownloadURL: "https://example.com", + URL: ec.String("repo://1234"), + FileMetadata: &models.ExtensionFileMetadata{ + LastModifiedDate: lastModified(), + Size: 1000, + }, + }), + ) +} + +func lastModified() strfmt.DateTime { + lastModified, _ := strfmt.ParseDateTime("2021-01-07T22:13:42.999Z") + return lastModified +} + +func protoV6ProviderFactoriesWithMockClient(client *api.API) map[string]func() (tfprotov6.ProviderServer, error) { + return map[string]func() (tfprotov6.ProviderServer, error){ + "ec": func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProtocol6(provider.ProviderWithClient(client, "unit-tests"))(), nil + }, + } +} diff --git a/ec/ecresource/extensionresource/schema.go b/ec/ecresource/extensionresource/schema.go index ee3ae22cb..7d323772a 100644 --- a/ec/ecresource/extensionresource/schema.go +++ b/ec/ecresource/extensionresource/schema.go @@ -18,64 +18,151 @@ package extensionresource import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" + "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" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/terraform-provider-ec/ec/internal/planmodifier" + + "github.com/elastic/terraform-provider-ec/ec/internal" ) -func newSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Description: "Required name of the ruleset", - Required: true, - }, - "description": { - Type: schema.TypeString, - Description: "Description for extension", - Optional: true, - }, - "extension_type": { - Type: schema.TypeString, - Description: "Extension type. bundle or plugin", - Required: true, - }, - "version": { - Type: schema.TypeString, - Description: "Eleasticsearch version", - Required: true, - }, - "download_url": { - Type: schema.TypeString, - Description: "download url", - Optional: true, - }, +// Ensure provider defined types fully satisfy framework interfaces +var _ resource.Resource = &Resource{} +var _ resource.ResourceWithConfigure = &Resource{} +var _ resource.ResourceWithImportState = &Resource{} +var _ resource.ResourceWithConfigValidators = &Resource{} - // Uploading file via API - "file_path": { - Type: schema.TypeString, - Description: "file path", - Optional: true, - RequiredWith: []string{"file_hash"}, - }, - "file_hash": { - Type: schema.TypeString, - Description: "file hash", - Optional: true, - }, +func (r *Resource) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "name": { + Type: types.StringType, + Description: "Required name of the ruleset", + Required: true, + }, + "description": { + Type: types.StringType, + Description: "Description for extension", + Computed: true, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifier.DefaultValue(types.String{Value: ""}), + }}, + "extension_type": { + Type: types.StringType, + Description: "Extension type. bundle or plugin", + Required: true, + }, + "version": { + Type: types.StringType, + Description: "Elasticsearch version", + Required: true, + }, + "download_url": { + Type: types.StringType, + Description: "download url", + Computed: true, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifier.DefaultValue(types.String{Value: ""}), + }, + }, - "url": { - Type: schema.TypeString, - Description: "", - Computed: true, - }, - "last_modified": { - Type: schema.TypeString, - Description: "", - Computed: true, - }, - "size": { - Type: schema.TypeInt, - Description: "", - Computed: true, + // Uploading file via API + "file_path": { + Type: types.StringType, + Description: "file path", + Optional: true, + }, + "file_hash": { + Type: types.StringType, + Description: "file hash", + Optional: true, + }, + "url": { + Type: types.StringType, + Description: "", + Computed: true, + }, + "last_modified": { + Type: types.StringType, + Description: "", + Computed: true, + }, + "size": { + Type: types.Int64Type, + Description: "", + Computed: true, + }, + // Computed attributes + "id": { + Type: types.StringType, + Computed: true, + MarkdownDescription: "Unique identifier of this resource.", + PlanModifiers: tfsdk.AttributePlanModifiers{ + resource.UseStateForUnknown(), + }, + }, }, + }, nil +} + +func (r *Resource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.RequiredTogether( + path.MatchRoot("file_path"), + path.MatchRoot("file_hash"), + ), } } + +type Resource struct { + client *api.API +} + +func (r *Resource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("id"), request.ID)...) +} + +func resourceReady(r *Resource, dg *diag.Diagnostics) bool { + if r.client == nil { + dg.AddError( + "Unconfigured API Client", + "Expected configured API client. Please report this issue to the provider developers.", + ) + + return false + } + return true +} + +func (r *Resource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + client, diags := internal.ConvertProviderData(request.ProviderData) + response.Diagnostics.Append(diags...) + r.client = client +} + +func (r *Resource) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_deployment_extension" +} + +type modelV0 struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + ExtensionType types.String `tfsdk:"extension_type"` + Version types.String `tfsdk:"version"` + DownloadURL types.String `tfsdk:"download_url"` + FilePath types.String `tfsdk:"file_path"` + FileHash types.String `tfsdk:"file_hash"` + URL types.String `tfsdk:"url"` + LastModified types.String `tfsdk:"last_modified"` + Size types.Int64 `tfsdk:"size"` +} diff --git a/ec/ecresource/extensionresource/testutil_datastruct.go b/ec/ecresource/extensionresource/testutil_datastruct.go deleted file mode 100644 index 61a79ccdb..000000000 --- a/ec/ecresource/extensionresource/testutil_datastruct.go +++ /dev/null @@ -1,47 +0,0 @@ -// 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 extensionresource - -func newExtension() map[string]interface{} { - return map[string]interface{}{ - "name": "my_extension", - "extension_type": "bundle", - "description": "my description", - "version": "*", - "download_url": "https://example.com", - "url": "repo://1234", - "last_modified": "2021-01-07T22:13:42.999Z", - "size": 1000, - } -} - -func newExtensionWithFilePath() map[string]interface{} { - return map[string]interface{}{ - "name": "my_extension", - "extension_type": "bundle", - "description": "my description", - "version": "*", - "download_url": "https://example.com", - "url": "repo://1234", - "last_modified": "2021-01-07T22:13:42.999Z", - "size": 1000, - - "file_path": "testdata/test_extension_bundle.json", - "file_hash": "abcd", - } -} diff --git a/ec/ecresource/extensionresource/update.go b/ec/ecresource/extensionresource/update.go index 613f0a780..1cb3b37e9 100644 --- a/ec/ecresource/extensionresource/update.go +++ b/ec/ecresource/extensionresource/update.go @@ -1,72 +1,74 @@ -// 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 extensionresource import ( "context" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/elastic/cloud-sdk-go/pkg/api" "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/extensionapi" - "github.com/elastic/cloud-sdk-go/pkg/models" - "github.com/elastic/cloud-sdk-go/pkg/multierror" ) -func updateResource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*api.API) +func (r *Resource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + if !resourceReady(r, &response.Diagnostics) { + return + } - _, err := updateRequest(client, d) - if err != nil { - return diag.FromErr(err) + var oldState modelV0 + var newState modelV0 + + diags := request.State.Get(ctx, &oldState) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return } - if _, ok := d.GetOk("file_path"); ok && d.HasChanges("file_hash", "last_modified", "size") { - if err := uploadExtension(client, d); err != nil { - return diag.FromErr(multierror.NewPrefixed("failed to upload file", err)) - } + diags = request.Plan.Get(ctx, &newState) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return } - return readResource(ctx, d, meta) -} + _, err := extensionapi.Update( + extensionapi.UpdateParams{ + API: r.client, + ExtensionID: newState.ID.Value, + Name: newState.Name.Value, + Version: newState.Version.Value, + Type: newState.ExtensionType.Value, + Description: newState.Description.Value, + DownloadURL: newState.DownloadURL.Value, + }, + ) + if err != nil { + response.Diagnostics.AddError(err.Error(), err.Error()) + return + } -func updateRequest(client *api.API, d *schema.ResourceData) (*models.Extension, error) { - name := d.Get("name").(string) - version := d.Get("version").(string) - extensionType := d.Get("extension_type").(string) - description := d.Get("description").(string) - downloadURL := d.Get("download_url").(string) + hasChanges := !oldState.FileHash.Equal(newState.FileHash) || + !oldState.LastModified.Equal(newState.LastModified) || + !oldState.Size.Equal(newState.Size) - body := extensionapi.UpdateParams{ - API: client, - ExtensionID: d.Id(), - Name: name, - Version: version, - Type: extensionType, - Description: description, - DownloadURL: downloadURL, + if !newState.FilePath.IsNull() && newState.FilePath.Value != "" && hasChanges { + response.Diagnostics.Append(r.uploadExtension(newState)...) + if response.Diagnostics.HasError() { + return + } } - res, err := extensionapi.Update(body) - if err != nil { - return nil, err + found, diags := r.read(newState.ID.Value, &newState) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + if !found { + response.Diagnostics.AddError( + "Failed to read deployment extension after update.", + "Failed to read deployment extension after update.", + ) + response.State.RemoveResource(ctx) + return } - return res, nil + // Finally, set the state + response.Diagnostics.Append(response.State.Set(ctx, newState)...) } diff --git a/ec/ecresource/extensionresource/update_test.go b/ec/ecresource/extensionresource/update_test.go deleted file mode 100644 index 6c42b610f..000000000 --- a/ec/ecresource/extensionresource/update_test.go +++ /dev/null @@ -1,194 +0,0 @@ -// 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 extensionresource - -import ( - "context" - "testing" - - "github.com/go-openapi/strfmt" - "github.com/stretchr/testify/assert" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/elastic/cloud-sdk-go/pkg/api" - "github.com/elastic/cloud-sdk-go/pkg/api/mock" - "github.com/elastic/cloud-sdk-go/pkg/models" - "github.com/elastic/cloud-sdk-go/pkg/util/ec" - - "github.com/elastic/terraform-provider-ec/ec/internal/util" -) - -func Test_updateResource(t *testing.T) { - tc200withoutFilePath := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - - wantTC200statewithoutFilePath := newExtension() - wantTC200statewithoutFilePath["name"] = "updated_extension" - wantTC200withoutFilePath := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: wantTC200statewithoutFilePath, - Schema: newSchema(), - }) - - tc200withFilePath := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtensionWithFilePath(), - Schema: newSchema(), - }) - wantTC200statewithFilePath := newExtensionWithFilePath() - wantTC200statewithFilePath["name"] = "updated_extension" - wantTC200withFilePath := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: wantTC200statewithFilePath, - Schema: newSchema(), - }) - - tc500Err := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - wantTC500 := util.NewResourceData(t, util.ResDataParams{ - ID: "12345678", - State: newExtension(), - Schema: newSchema(), - }) - - lastModified, _ := strfmt.ParseDateTime("2021-01-07T22:13:42.999Z") - type args struct { - ctx context.Context - d *schema.ResourceData - meta interface{} - } - tests := []struct { - name string - args args - want diag.Diagnostics - wantRD *schema.ResourceData - }{ - { - name: "returns nil when it receives a 200 without file_path", - args: args{ - d: tc200withoutFilePath, - meta: api.NewMock( - mock.New200StructResponse(models.Extension{ // update request response - Name: ec.String("updated_extension"), - ExtensionType: ec.String("bundle"), - Description: "my description", - Version: ec.String("*"), - DownloadURL: "https://example.com", - URL: ec.String("repo://1234"), - FileMetadata: &models.ExtensionFileMetadata{ - LastModifiedDate: lastModified, - Size: 1000, - }, - }), - mock.New200StructResponse(models.Extension{ // read request response - Name: ec.String("updated_extension"), - ExtensionType: ec.String("bundle"), - Description: "my description", - Version: ec.String("*"), - DownloadURL: "https://example.com", - URL: ec.String("repo://1234"), - FileMetadata: &models.ExtensionFileMetadata{ - LastModifiedDate: lastModified, - Size: 1000, - }, - }), - ), - }, - want: nil, - wantRD: wantTC200withoutFilePath, - }, - { - name: "returns nil when it receives a 200 with file_path", - args: args{ - d: tc200withFilePath, - meta: api.NewMock( - mock.New200StructResponse(models.Extension{ // update request response - Name: ec.String("updated_extension"), - ExtensionType: ec.String("bundle"), - Description: "my description", - Version: ec.String("*"), - DownloadURL: "https://example.com", - URL: ec.String("repo://1234"), - FileMetadata: &models.ExtensionFileMetadata{ - LastModifiedDate: lastModified, - Size: 1000, - }, - }), - mock.New200StructResponse(nil), // upload request response - mock.New200StructResponse(models.Extension{ // read request response - Name: ec.String("updated_extension"), - ExtensionType: ec.String("bundle"), - Description: "my description", - Version: ec.String("*"), - DownloadURL: "https://example.com", - URL: ec.String("repo://1234"), - FileMetadata: &models.ExtensionFileMetadata{ - LastModifiedDate: lastModified, - Size: 1000, - }, - }), - ), - }, - want: nil, - wantRD: wantTC200withFilePath, - }, - { - name: "returns an error when it receives a 500", - args: args{ - d: tc500Err, - meta: api.NewMock(mock.NewErrorResponse(500, mock.APIError{ - Code: "some", Message: "message", - })), - }, - want: diag.Diagnostics{ - { - Severity: diag.Error, - Summary: "api error: 1 error occurred:\n\t* some: message\n\n", - }, - }, - wantRD: wantTC500, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := updateResource(tt.args.ctx, tt.args.d, tt.args.meta) - assert.Equal(t, tt.want, got) - var want interface{} - if tt.wantRD != nil { - if s := tt.wantRD.State(); s != nil { - want = s.Attributes - } - } - - var gotState interface{} - if s := tt.args.d.State(); s != nil { - gotState = s.Attributes - } - - assert.Equal(t, want, gotState) - }) - } -} diff --git a/ec/ecresource/extensionresource/upload.go b/ec/ecresource/extensionresource/upload.go index d05851575..a3e53d36e 100644 --- a/ec/ecresource/extensionresource/upload.go +++ b/ec/ecresource/extensionresource/upload.go @@ -20,28 +20,29 @@ package extensionresource import ( "os" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/elastic/cloud-sdk-go/pkg/api" "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/extensionapi" - "github.com/elastic/cloud-sdk-go/pkg/multierror" ) -func uploadExtension(client *api.API, d *schema.ResourceData) error { - filePath := d.Get("file_path").(string) - reader, err := os.Open(filePath) +func (r *Resource) uploadExtension(state modelV0) diag.Diagnostics { + var diags diag.Diagnostics + + reader, err := os.Open(state.FilePath.Value) if err != nil { - return multierror.NewPrefixed("failed to open file", err) + diags.AddError("failed to open file", err.Error()) + return diags } _, err = extensionapi.Upload(extensionapi.UploadParams{ - API: client, - ExtensionID: d.Id(), + API: r.client, + ExtensionID: state.ID.Value, File: reader, }) if err != nil { - return err + diags.AddError("failed to upload file", err.Error()) + return diags } - return nil + return diags } diff --git a/ec/ecresource/trafficfilterresource/create.go b/ec/ecresource/trafficfilterresource/create.go index bc3f8614f..25c05136a 100644 --- a/ec/ecresource/trafficfilterresource/create.go +++ b/ec/ecresource/trafficfilterresource/create.go @@ -59,7 +59,12 @@ func (r Resource) Create(ctx context.Context, request resource.CreateRequest, re found, diags := r.read(ctx, newState.ID.Value, &newState) response.Diagnostics.Append(diags...) if !found { + response.Diagnostics.AddError( + "Failed to read deployment traffic filter ruleset after create.", + "Failed to read deployment traffic filter ruleset after create.", + ) response.State.RemoveResource(ctx) + return } if response.Diagnostics.HasError() { return diff --git a/ec/ecresource/trafficfilterresource/expanders.go b/ec/ecresource/trafficfilterresource/expanders.go index a60a9721d..a4f07d0d4 100644 --- a/ec/ecresource/trafficfilterresource/expanders.go +++ b/ec/ecresource/trafficfilterresource/expanders.go @@ -23,7 +23,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/elastic/cloud-sdk-go/pkg/models" - "github.com/elastic/cloud-sdk-go/pkg/util/ec" ) func expandModel(ctx context.Context, state modelV0) (*models.TrafficFilterRulesetRequest, diag.Diagnostics) { @@ -36,11 +35,11 @@ func expandModel(ctx context.Context, state modelV0) (*models.TrafficFilterRules } var request = models.TrafficFilterRulesetRequest{ - Name: ec.String(state.Name.Value), - Type: ec.String(state.Type.Value), - Region: ec.String(state.Region.Value), + Name: &state.Name.Value, + Type: &state.Type.Value, + Region: &state.Region.Value, Description: state.Description.Value, - IncludeByDefault: ec.Bool(state.IncludeByDefault.Value), + IncludeByDefault: &state.IncludeByDefault.Value, Rules: make([]*models.TrafficFilterRule, 0, len(ruleSet)), } diff --git a/ec/ecresource/trafficfilterresource/resource_test.go b/ec/ecresource/trafficfilterresource/resource_test.go index ac39c0cdd..beb22c8ec 100644 --- a/ec/ecresource/trafficfilterresource/resource_test.go +++ b/ec/ecresource/trafficfilterresource/resource_test.go @@ -125,7 +125,7 @@ func TestResourceTrafficFilter_failedRead1(t *testing.T) { }) } -func TestResourceTrafficFilter_gracefulDeletionOnUpdate(t *testing.T) { +func TestResourceTrafficFilter_notFoundAfterUpdate(t *testing.T) { r.UnitTest(t, r.TestCase{ ProtoV6ProviderFactories: protoV6ProviderFactoriesWithMockClient( api.NewMock( @@ -135,7 +135,7 @@ func TestResourceTrafficFilter_gracefulDeletionOnUpdate(t *testing.T) { readResponse("false", "true"), updateResponse("false"), notFoundReadResponse("false"), - notFoundReadResponse("false"), + notFoundReadResponse("true"), ), ), Steps: []r.TestStep{ @@ -144,9 +144,8 @@ func TestResourceTrafficFilter_gracefulDeletionOnUpdate(t *testing.T) { Check: checkResource("true"), }, { // Update resource - Config: trafficFilterWithoutIncludeByDefault, - Check: checkResource("false"), // Update can't remove the resource, so it should stay the same. - ExpectNonEmptyPlan: true, // terraform refresh will detect the removed resource, so we will end up with a non-empty plan. + Config: trafficFilterWithoutIncludeByDefault, + ExpectError: regexp.MustCompile(`Failed to read deployment traffic filter ruleset after update.`), }, }, }) diff --git a/ec/ecresource/trafficfilterresource/update.go b/ec/ecresource/trafficfilterresource/update.go index fdbe03cde..50af50f15 100644 --- a/ec/ecresource/trafficfilterresource/update.go +++ b/ec/ecresource/trafficfilterresource/update.go @@ -40,6 +40,7 @@ func (r Resource) Update(ctx context.Context, request resource.UpdateRequest, re } trafficFilterRulesetRequest, diags := expandModel(ctx, newState) + response.Diagnostics.Append(diags...) _, err := trafficfilterapi.Update(trafficfilterapi.UpdateParams{ API: r.client, ID: newState.ID.Value, Req: trafficFilterRulesetRequest, @@ -55,12 +56,12 @@ func (r Resource) Update(ctx context.Context, request resource.UpdateRequest, re return } if !found { - // We can't unset the state here, and must make sure to set the state according to the plan below. - // So all we do is add a warning. - diags.AddWarning( - "Failed to read traffic filter rule.", - "Please run terraform refresh to ensure a consistent state.", + response.Diagnostics.AddError( + "Failed to read deployment traffic filter ruleset after update.", + "Failed to read deployment traffic filter ruleset after update.", ) + response.State.RemoveResource(ctx) + return } // Finally, set the state diff --git a/ec/internal/util/helpers.go b/ec/internal/util/helpers.go index 4abc5b5c7..960ab8c19 100644 --- a/ec/internal/util/helpers.go +++ b/ec/internal/util/helpers.go @@ -132,6 +132,7 @@ func StringListAsType(in []string) types.List { } return types.List{ElemType: types.StringType, Elems: out} } + func StringMapAsType(in map[string]string) types.Map { //goland:noinspection GoPreferNilSlice out := make(map[string]attr.Value, len(in)) diff --git a/ec/provider.go b/ec/provider.go index d076cc8af..3c243cf07 100644 --- a/ec/provider.go +++ b/ec/provider.go @@ -78,8 +78,7 @@ func LegacyProvider() *schema.Provider { Schema: newSchema(), DataSourcesMap: map[string]*schema.Resource{}, ResourcesMap: map[string]*schema.Resource{ - "ec_deployment": deploymentresource.Resource(), - "ec_deployment_extension": extensionresource.Resource(), + "ec_deployment": deploymentresource.Resource(), }, } } @@ -170,6 +169,7 @@ func (p *Provider) DataSources(ctx context.Context) []func() datasource.DataSour func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ func() resource.Resource { return &elasticsearchkeystoreresource.Resource{} }, + func() resource.Resource { return &extensionresource.Resource{} }, func() resource.Resource { return &trafficfilterresource.Resource{} }, func() resource.Resource { return &trafficfilterassocresource.Resource{} }, }