diff --git a/docs/resources/ec_extension.md b/docs/resources/ec_extension.md new file mode 100644 index 000000000..5acdf43d3 --- /dev/null +++ b/docs/resources/ec_extension.md @@ -0,0 +1,60 @@ +--- +page_title: "Elastic Cloud: ec_extension" +description: |- + Provides an Elastic Cloud extension resource, which allows extensions to be created, updated, and deleted. +--- + +# Resource: ec_extension +Provides an Elastic Cloud extension resource, which allows extensions to be created, updated, and deleted. + +Extensions allow users of Elastic Cloud to use custom plugins, scripts, or dictionaries to enhance the core functionality of Elasticsearch. Before you install an extension, be sure to check out the supported and official [Elasticsearch plugins](https://www.elastic.co/guide/en/elasticsearch/plugins/current/index.html) already available. + +## Example Usage +### With extension file + +```hcl +locals { + file_path = "/path/to/plugin.zip" +} + +resource "ec_extension" "example_extension" { + name = "my_extension" + description = "my extension" + version = "*" + extension_type = "bundle" + + file_path = local.file_path + file_hash = filebase64sha256(local.file_path) +} +``` + +### With download URL +```hcl +resource "ec_extension" "example_extension" { + name = "my_extension" + description = "my extension" + version = "*" + extension_type = "bundle" + download_url = "https://example.net" +} +``` + +## Argument Reference +The following arguments are supported: + +* `name` - (Required) Name of the extension. +* `description` - (Optional) Description of the extension. +* `extension_type` - (Required) `bundle` or `plugin` allowed. A `bundle` will usually contain a dictionary or script, where a `plugin` is compiled from source. +* `version` - (Required) Elastic stack version, a numeric version for plugins, e.g. 2.3.0 should be set. Major version e.g. 2.*, or wildcards e.g. * for bundles. +* `download_url` - (Optional) The URL to download the extension archive. +* `file_path` - (Optional) File path of the extension uploaded. +* `file_hash` - (Optional) Hash value of the file. If it is changed, the file is reuploaded. + + +## Attributes Reference +In addition to all the arguments above, the following attributes are exported: + +* `id` - Extension identifier. +* `url` - The extension URL to be used in the plan. +* `last_modified` - The datetime the extension was last modified. +* `size` - The extension file size in bytes. diff --git a/ec/acc/extension_basic_test.go b/ec/acc/extension_basic_test.go new file mode 100644 index 000000000..1380ed8a0 --- /dev/null +++ b/ec/acc/extension_basic_test.go @@ -0,0 +1,73 @@ +// 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 acc + +import ( + "fmt" + "io/ioutil" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccExtension_basic(t *testing.T) { + resName := "ec_extension.my_extension" + randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + cfg := fixtureAccExtensionBasicWithTF(t, "testdata/extension_basic.tf", randomName, "desc") + cfg2 := fixtureAccExtensionBasicWithTF(t, "testdata/extension_basic.tf", randomName, "updated desc") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactory, + CheckDestroy: testAccExtensionDestroy, + Steps: []resource.TestStep{ + { + 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"), + ), + }, + { + Config: cfg2, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resName, "name", randomName), + resource.TestCheckResourceAttr(resName, "version", "*"), + resource.TestCheckResourceAttr(resName, "description", "updated desc"), + resource.TestCheckResourceAttr(resName, "extension_type", "bundle"), + ), + }, + }, + }) +} + +func fixtureAccExtensionBasicWithTF(t *testing.T, tfFileName, extensionName, description string) string { + t.Helper() + + b, err := ioutil.ReadFile(tfFileName) + if err != nil { + t.Fatal(err) + } + return fmt.Sprintf(string(b), + extensionName, description, + ) +} diff --git a/ec/acc/extension_bundle_file_test.go b/ec/acc/extension_bundle_file_test.go new file mode 100644 index 000000000..330323de8 --- /dev/null +++ b/ec/acc/extension_bundle_file_test.go @@ -0,0 +1,177 @@ +// 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 acc + +import ( + "archive/zip" + "bytes" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/elastic/cloud-sdk-go/pkg/client/extensions" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccExtension_bundleFile(t *testing.T) { + resName := "ec_extension.my_extension" + randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + filePath := filepath.Join(os.TempDir(), "extension.zip") + defer os.Remove(filePath) + + cfg := fixtureAccExtensionBundleWithTF(t, "testdata/extension_bundle_file.tf", filePath, randomName, "desc") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactory, + CheckDestroy: testAccExtensionDestroy, + Steps: []resource.TestStep{ + { + PreConfig: func() { writeFile(t, filePath, "extension.txt", "foo") }, + 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"), + resource.TestCheckResourceAttr(resName, "file_path", filePath), + func(s *terraform.State) error { + return checkExtensionFile(t, s, "extension.txt", "foo") + }, + ), + }, + { + PreConfig: func() { writeFile(t, filePath, "extension.txt", "bar") }, + 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"), + resource.TestCheckResourceAttr(resName, "file_path", filePath), + func(s *terraform.State) error { + return checkExtensionFile(t, s, "extension.txt", "bar") + }, + ), + }, + }, + }) +} + +func fixtureAccExtensionBundleWithTF(t *testing.T, tfFileName, bundleFilePath, extensionName, description string) string { + t.Helper() + + b, err := ioutil.ReadFile(tfFileName) + if err != nil { + t.Fatal(err) + } + return fmt.Sprintf(string(b), + bundleFilePath, extensionName, description, + ) +} + +func writeFile(t *testing.T, filePath, fileName, content string) { + t.Helper() + + buf := new(bytes.Buffer) + writer := zip.NewWriter(buf) + + f, err := writer.Create(fileName) + if err != nil { + t.Fatal(err) + } + + if _, err := f.Write([]byte(content)); err != nil { + t.Fatal(err) + } + + if err := writer.Close(); err != nil { + t.Fatal(err) + } + + if err := ioutil.WriteFile(filePath, buf.Bytes(), 0644); err != nil { + t.Fatal(err) + } +} + +func checkExtensionFile(t *testing.T, s *terraform.State, filename string, expected string) error { + client, err := newAPI() + if err != nil { + t.Fatal(err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "ec_extension" { + continue + } + + res, err := client.V1API.Extensions.GetExtension( + extensions.NewGetExtensionParams().WithExtensionID(rs.Primary.ID), + client.AuthWriter) + if err != nil { + t.Fatal(err) + } + + content, err := downloadAndReadExtension(filename, res.Payload.FileMetadata.URL.String(), res.Payload.FileMetadata.Size) + if err != nil { + t.Fatal(err) + } + + if content == expected { + return nil // ok + } + return fmt.Errorf("extension content is expected: %s, but got: %s", expected, content) + } + + return fmt.Errorf("extension doesn't exists") +} + +func downloadAndReadExtension(filename string, url string, size int64) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", err + } + + b, _ := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + + r, err := zip.NewReader(bytes.NewReader(b), size) + if err != nil { + return "", err + } + + if len(r.File) == 0 { + return "", fmt.Errorf("the zip file has no content") + } + + for _, f := range r.File { + reader, _ := f.Open() + b, _ := ioutil.ReadAll(reader) + func() { defer reader.Close() }() + if filename == f.Name { + return string(b), nil + } + } + return "", fmt.Errorf("not found: %s", filename) +} diff --git a/ec/acc/extension_destroy_test.go b/ec/acc/extension_destroy_test.go new file mode 100644 index 000000000..25c07fe86 --- /dev/null +++ b/ec/acc/extension_destroy_test.go @@ -0,0 +1,52 @@ +// 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 acc + +import ( + "fmt" + + "github.com/elastic/cloud-sdk-go/pkg/api/apierror" + "github.com/elastic/cloud-sdk-go/pkg/client/extensions" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func testAccExtensionDestroy(s *terraform.State) error { + client, err := newAPI() + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "ec_extension" { + continue + } + + res, err := client.V1API.Extensions.GetExtension( + extensions.NewGetExtensionParams().WithExtensionID(rs.Primary.ID), + client.AuthWriter) + + // If not extension exists, api gets 403 error + if err != nil && apierror.IsRuntimeStatusCode(err, 403) { + continue + } + + return fmt.Errorf("extension (%s) still exists", *res.Payload.ID) + } + + return nil +} diff --git a/ec/acc/extension_plugin_download_test.go b/ec/acc/extension_plugin_download_test.go new file mode 100644 index 000000000..bd88565ac --- /dev/null +++ b/ec/acc/extension_plugin_download_test.go @@ -0,0 +1,62 @@ +// 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 acc + +import ( + "fmt" + "io/ioutil" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccExtension_pluginDownload(t *testing.T) { + resName := "ec_extension.my_extension" + randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + downloadURL := "https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-icu/analysis-icu-7.10.1.zip" + + cfg := fixtureAccExtensionBundleDownloadWithTF(t, "testdata/extension_plugin_download.tf", randomName, downloadURL) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactory, + CheckDestroy: testAccExtensionDestroy, + Steps: []resource.TestStep{ + { + Config: cfg, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resName, "name", randomName), + resource.TestCheckResourceAttr(resName, "version", "7.10.1"), + resource.TestCheckResourceAttr(resName, "download_url", downloadURL), + resource.TestCheckResourceAttr(resName, "extension_type", "plugin"), + ), + }, + }, + }) +} + +func fixtureAccExtensionBundleDownloadWithTF(t *testing.T, tfFileName, extensionName, downloadURL string) string { + t.Helper() + + b, err := ioutil.ReadFile(tfFileName) + if err != nil { + t.Fatal(err) + } + return fmt.Sprintf(string(b), extensionName, downloadURL) +} diff --git a/ec/acc/testdata/extension_basic.tf b/ec/acc/testdata/extension_basic.tf new file mode 100644 index 000000000..21c7fc456 --- /dev/null +++ b/ec/acc/testdata/extension_basic.tf @@ -0,0 +1,6 @@ +resource "ec_extension" "my_extension" { + name = "%s" + description = "%s" + version = "*" + extension_type = "bundle" +} diff --git a/ec/acc/testdata/extension_bundle_file.tf b/ec/acc/testdata/extension_bundle_file.tf new file mode 100644 index 000000000..e731e65f4 --- /dev/null +++ b/ec/acc/testdata/extension_bundle_file.tf @@ -0,0 +1,13 @@ +locals { + file_path = "%s" +} + +resource "ec_extension" "my_extension" { + name = "%s" + description = "%s" + version = "*" + extension_type = "bundle" + + file_path = local.file_path + file_hash = filebase64sha256(local.file_path) +} diff --git a/ec/acc/testdata/extension_plugin_download.tf b/ec/acc/testdata/extension_plugin_download.tf new file mode 100644 index 000000000..6122557d3 --- /dev/null +++ b/ec/acc/testdata/extension_plugin_download.tf @@ -0,0 +1,6 @@ +resource "ec_extension" "my_extension" { + name = "%s" + version = "7.10.1" + extension_type = "plugin" + download_url = "%s" +} diff --git a/ec/ecresource/extensionresource/create.go b/ec/ecresource/extensionresource/create.go new file mode 100644 index 000000000..70394e102 --- /dev/null +++ b/ec/ecresource/extensionresource/create.go @@ -0,0 +1,46 @@ +// 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/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// createResource will create a new deployment extension +func createResource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*api.API) + + model, err := createRequest(client, d) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(*model.ID) + + if _, ok := d.GetOk("file_path"); ok { + if err := uploadRequest(client, d); err != nil { + return diag.FromErr(multierror.NewPrefixed("failed to upload file", err)) + } + } + return readResource(ctx, d, meta) +} diff --git a/ec/ecresource/extensionresource/create_test.go b/ec/ecresource/extensionresource/create_test.go new file mode 100644 index 000000000..2c33b134f --- /dev/null +++ b/ec/ecresource/extensionresource/create_test.go @@ -0,0 +1,92 @@ +// 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/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/stretchr/testify/assert" + + "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 new file mode 100644 index 000000000..c51b7a2c0 --- /dev/null +++ b/ec/ecresource/extensionresource/delete.go @@ -0,0 +1,49 @@ +// 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/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/client/extensions" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func deleteResource(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*api.API) + + if err := deleteRequest(client, d); err != nil { + if alreadyDestroyed(err) { + d.SetId("") + return nil + } + + return diag.FromErr(err) + } + + d.SetId("") + return nil +} + +func alreadyDestroyed(err error) bool { + var extensionNotFound *extensions.DeleteExtensionNotFound + return errors.As(err, &extensionNotFound) +} diff --git a/ec/ecresource/extensionresource/delete_test.go b/ec/ecresource/extensionresource/delete_test.go new file mode 100644 index 000000000..b8f2af718 --- /dev/null +++ b/ec/ecresource/extensionresource/delete_test.go @@ -0,0 +1,136 @@ +// 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/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/stretchr/testify/assert" + + "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 new file mode 100644 index 000000000..9529d33f2 --- /dev/null +++ b/ec/ecresource/extensionresource/read.go @@ -0,0 +1,96 @@ +// 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/elastic/cloud-sdk-go/pkg/api" + "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" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func readResource(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*api.API) + + model, err := readRequest(d, client) + + if err != nil { + if extensionNotFound(err) { + d.SetId("") + return nil + } + + return diag.FromErr(multierror.NewPrefixed("failed reading extension", err)) + } + + if err := modelToState(d, model); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func extensionNotFound(err error) bool { + // We're using the As() call since we do not care about the error value + // but do care about the error's contents type since it's an implicit 404. + var extensionNotFound *extensions.GetExtensionNotFound + 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 + } + + if err := d.Set("version", model.Version); err != nil { + return err + } + + if err := d.Set("extension_type", model.ExtensionType); err != nil { + return err + } + + if err := d.Set("description", model.Description); err != nil { + return err + } + + if err := d.Set("url", model.URL); err != nil { + return err + } + + if err := d.Set("download_url", model.DownloadURL); err != nil { + return err + } + + 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 + } + } + + return nil +} diff --git a/ec/ecresource/extensionresource/read_test.go b/ec/ecresource/extensionresource/read_test.go new file mode 100644 index 000000000..a70d4ee49 --- /dev/null +++ b/ec/ecresource/extensionresource/read_test.go @@ -0,0 +1,152 @@ +// 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/elastic/cloud-sdk-go/pkg/util/ec" + + "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/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/stretchr/testify/assert" + + "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: "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/request.go b/ec/ecresource/extensionresource/request.go new file mode 100644 index 000000000..167eb6bea --- /dev/null +++ b/ec/ecresource/extensionresource/request.go @@ -0,0 +1,118 @@ +// 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 ( + "os" + + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/apierror" + "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" + "github.com/go-openapi/runtime" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func readRequest(d *schema.ResourceData, client *api.API) (*models.Extension, error) { + res, err := client.V1API.Extensions.GetExtension( + extensions.NewGetExtensionParams().WithExtensionID(d.Id()), + client.AuthWriter) + + if err != nil { + return nil, apierror.Wrap(err) + } + return res.Payload, nil +} + +func createRequest(client *api.API, d *schema.ResourceData) (*models.Extension, error) { + name := d.Get("name").(string) + version := d.Get("version").(string) + extensionsType := d.Get("extension_type").(string) + description := d.Get("description").(string) + downloadURL := d.Get("download_url").(string) + + body := &models.CreateExtensionRequest{ + Name: &name, + Version: &version, + ExtensionType: &extensionsType, + Description: description, + DownloadURL: downloadURL, + } + + res, err := client.V1API.Extensions.CreateExtension( + extensions.NewCreateExtensionParams().WithBody(body), + client.AuthWriter) + + if err != nil { + return nil, apierror.Wrap(err) + } + return res.Payload, nil +} + +func updateRequest(client *api.API, d *schema.ResourceData) (*models.Extension, error) { + name := d.Get("name").(string) + version := d.Get("version").(string) + extensionsType := d.Get("extension_type").(string) + description := d.Get("description").(string) + downloadURL := d.Get("download_url").(string) + + body := &models.UpdateExtensionRequest{ + Name: &name, + Version: &version, + ExtensionType: &extensionsType, + Description: description, + DownloadURL: downloadURL, + } + + res, err := client.V1API.Extensions.UpdateExtension( + extensions.NewUpdateExtensionParams().WithBody(body).WithExtensionID(d.Id()), + client.AuthWriter) + if err != nil { + return nil, apierror.Wrap(err) + } + + return res.Payload, nil +} + +func deleteRequest(client *api.API, d *schema.ResourceData) error { + if _, err := client.V1API.Extensions.DeleteExtension( + extensions.NewDeleteExtensionParams().WithExtensionID(d.Id()), + client.AuthWriter); err != nil { + return apierror.Wrap(err) + } + + return nil +} + +func uploadRequest(client *api.API, d *schema.ResourceData) error { + filePath := d.Get("file_path").(string) + reader, err := os.Open(filePath) + if err != nil { + return multierror.NewPrefixed("failed open file", err) + } + + if _, err := client.V1API.Extensions.UploadExtension( + extensions.NewUploadExtensionParams().WithExtensionID(d.Id()). + WithFile(runtime.NamedReader(filePath, reader)), + client.AuthWriter); err != nil { + return apierror.Wrap(err) + } + + return nil +} diff --git a/ec/ecresource/extensionresource/resource.go b/ec/ecresource/extensionresource/resource.go new file mode 100644 index 000000000..41fd94d04 --- /dev/null +++ b/ec/ecresource/extensionresource/resource.go @@ -0,0 +1,41 @@ +// 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_extension resource schema. +func Resource() *schema.Resource { + return &schema.Resource{ + Description: "Elastic Cloud extension (plugin or bundle (scripts or dictionaries)) 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, + + Timeouts: &schema.ResourceTimeout{ + Default: schema.DefaultTimeout(10 * time.Minute), + }, + } +} diff --git a/ec/ecresource/extensionresource/schema.go b/ec/ecresource/extensionresource/schema.go new file mode 100644 index 000000000..ee3ae22cb --- /dev/null +++ b/ec/ecresource/extensionresource/schema.go @@ -0,0 +1,81 @@ +// 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 ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +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, + }, + + // 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, + }, + + "url": { + Type: schema.TypeString, + Description: "", + Computed: true, + }, + "last_modified": { + Type: schema.TypeString, + Description: "", + Computed: true, + }, + "size": { + Type: schema.TypeInt, + Description: "", + Computed: true, + }, + } +} diff --git a/ec/ecresource/extensionresource/test_extension_bundle.json b/ec/ecresource/extensionresource/test_extension_bundle.json new file mode 100644 index 000000000..18d7acf58 --- /dev/null +++ b/ec/ecresource/extensionresource/test_extension_bundle.json @@ -0,0 +1 @@ +{"foo": "bar"} \ No newline at end of file diff --git a/ec/ecresource/extensionresource/testutil_datastruct.go b/ec/ecresource/extensionresource/testutil_datastruct.go new file mode 100644 index 000000000..6d168864e --- /dev/null +++ b/ec/ecresource/extensionresource/testutil_datastruct.go @@ -0,0 +1,47 @@ +// 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": "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": "example.com", + "url": "repo://1234", + "last_modified": "2021-01-07T22:13:42.999Z", + "size": 1000, + + "file_path": "test_extension_bundle.json", + "file_hash": "abcd", + } +} diff --git a/ec/ecresource/extensionresource/update.go b/ec/ecresource/extensionresource/update.go new file mode 100644 index 000000000..cfeb63284 --- /dev/null +++ b/ec/ecresource/extensionresource/update.go @@ -0,0 +1,44 @@ +// 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/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func updateResource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*api.API) + + _, err := updateRequest(client, d) + if err != nil { + return diag.FromErr(err) + } + + if _, ok := d.GetOk("file_path"); ok && d.HasChanges("file_hash", "last_modified", "size") { + if err := uploadRequest(client, d); err != nil { + return diag.FromErr(multierror.NewPrefixed("failed to upload file", err)) + } + } + + return readResource(ctx, d, meta) +} diff --git a/ec/ecresource/extensionresource/update_test.go b/ec/ecresource/extensionresource/update_test.go new file mode 100644 index 000000000..297bae5f1 --- /dev/null +++ b/ec/ecresource/extensionresource/update_test.go @@ -0,0 +1,192 @@ +// 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/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/go-openapi/strfmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/stretchr/testify/assert" + + "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: "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: "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: "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: "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/provider.go b/ec/provider.go index 0d17a4990..43710aa72 100644 --- a/ec/provider.go +++ b/ec/provider.go @@ -29,6 +29,7 @@ import ( "github.com/elastic/terraform-provider-ec/ec/ecdatasource/deploymentsdatasource" "github.com/elastic/terraform-provider-ec/ec/ecdatasource/stackdatasource" "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource" + "github.com/elastic/terraform-provider-ec/ec/ecresource/extensionresource" "github.com/elastic/terraform-provider-ec/ec/ecresource/trafficfilterassocresource" "github.com/elastic/terraform-provider-ec/ec/ecresource/trafficfilterresource" ) @@ -71,6 +72,7 @@ func Provider() *schema.Provider { "ec_deployment": deploymentresource.Resource(), "ec_deployment_traffic_filter": trafficfilterresource.Resource(), "ec_deployment_traffic_filter_association": trafficfilterassocresource.Resource(), + "ec_extension": extensionresource.Resource(), }, } } diff --git a/examples/extension_bundle/README.md b/examples/extension_bundle/README.md new file mode 100644 index 000000000..b7f2d2738 --- /dev/null +++ b/examples/extension_bundle/README.md @@ -0,0 +1,15 @@ +# Extension Bundle example + +This example shows how to create an Elastic Cloud bundle extension using Terraform. + +The bundle extension is created with `files/content.json` uploaded. + +See https://www.elastic.co/guide/en/cloud/current/ec-custom-bundles.html#ec-add-your-plugin for details. + +## Running the example + +To run the example, follow these steps: + +1. Build the provider by running `make install` from the main folder. +2. Run `terrafrom init` to initialize your Terraform CLI. +3. Run `terraform apply` to see how it works. diff --git a/examples/extension_bundle/extension.tf b/examples/extension_bundle/extension.tf new file mode 100644 index 000000000..16373ce11 --- /dev/null +++ b/examples/extension_bundle/extension.tf @@ -0,0 +1,27 @@ +terraform { + required_version = ">= 0.12.29" + + required_providers { + ec = { + source = "elastic/ec" + version = "0.1.0-beta" + } + } +} + +provider "ec" {} + +locals { + file_path = "./files/content.json.zip" +} + +# Create an Elastic Cloud Extension +resource "ec_extension" "example_extension" { + name = "my_extension" + description = "my extension" + version = "*" + extension_type = "bundle" + + file_path = local.file_path + file_hash = filebase64sha256(local.file_path) +} diff --git a/examples/extension_bundle/files/content.json b/examples/extension_bundle/files/content.json new file mode 100644 index 000000000..b18dda311 --- /dev/null +++ b/examples/extension_bundle/files/content.json @@ -0,0 +1 @@ +{"foo": "value", "bar": 3} \ No newline at end of file diff --git a/examples/extension_bundle/files/content.json.zip b/examples/extension_bundle/files/content.json.zip new file mode 100644 index 000000000..41b9d74f4 Binary files /dev/null and b/examples/extension_bundle/files/content.json.zip differ diff --git a/examples/extension_bundle/outputs.tf b/examples/extension_bundle/outputs.tf new file mode 100644 index 000000000..497f99678 --- /dev/null +++ b/examples/extension_bundle/outputs.tf @@ -0,0 +1,3 @@ +output "extension_id" { + value = ec_extension.example_extension.id +}