diff --git a/CHANGELOG.md b/CHANGELOG.md index f92cc810d..e1b9d8f85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ nav_order: 1 - Fix service IP filters normalization - Fix improper omitting in `ToAPI` - Fix Kafka Topic perfomance +- Add OpenSearch Security Plugin support (`aiven_opensearch_security_plugin_config` resource) ## [4.8.1] - 2023-08-23 diff --git a/docs/data-sources/opensearch_security_plugin_config.md b/docs/data-sources/opensearch_security_plugin_config.md new file mode 100644 index 000000000..294e364d6 --- /dev/null +++ b/docs/data-sources/opensearch_security_plugin_config.md @@ -0,0 +1,36 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_opensearch_security_plugin_config Data Source - terraform-provider-aiven" +subcategory: "" +description: |- + The OpenSearch Security Plugin Config data source provides information about an existing Aiven OpenSearch Security Plugin Config. +--- + +# aiven_opensearch_security_plugin_config (Data Source) + +The OpenSearch Security Plugin Config data source provides information about an existing Aiven OpenSearch Security Plugin Config. + +## Example Usage + +```terraform +data "aiven_opensearch_security_plugin_config" "os-sec-config" { + project = aiven_project.os-project.project + service_name = aiven_opensearch.os.service_name +} +``` + + +## Schema + +### Required + +- `project` (String) Identifies the project this resource belongs to. To set up proper dependencies please refer to this variable as a reference. This property cannot be changed, doing so forces recreation of the resource. +- `service_name` (String) Specifies the name of the service that this resource belongs to. To set up proper dependencies please refer to this variable as a reference. This property cannot be changed, doing so forces recreation of the resource. + +### Read-Only + +- `admin_enabled` (Boolean) Whether the os-sec-admin user is enabled. This indicates whether the user management with the security plugin is enabled. This is always true when the os-sec-admin password was set at least once. +- `admin_password` (String, Sensitive) The password for the os-sec-admin user. +- `available` (Boolean) Whether the security plugin is available. This is always true for recently created services. +- `enabled` (Boolean) Whether the security plugin is enabled. This is always true for recently created services. +- `id` (String) The ID of this resource. diff --git a/docs/resources/opensearch_acl_config.md b/docs/resources/opensearch_acl_config.md index 7996f7fa7..0b1092a23 100644 --- a/docs/resources/opensearch_acl_config.md +++ b/docs/resources/opensearch_acl_config.md @@ -3,18 +3,18 @@ page_title: "aiven_opensearch_acl_config Resource - terraform-provider-aiven" subcategory: "" description: |- - The OpenSearch resource allows the creation and management of Aiven OpenSearch services. + The OpenSearch ACL Config resource allows the creation and management of Aiven OpenSearch ACLs. --- # aiven_opensearch_acl_config (Resource) -The OpenSearch resource allows the creation and management of Aiven OpenSearch services. +The OpenSearch ACL Config resource allows the creation and management of Aiven OpenSearch ACLs. ## Example Usage ```terraform data "aiven_project" "foo" { - project = "example_project" + project = "example_project" } resource "aiven_opensearch" "bar" { diff --git a/docs/resources/opensearch_security_plugin_config.md b/docs/resources/opensearch_security_plugin_config.md new file mode 100644 index 000000000..9b48bec22 --- /dev/null +++ b/docs/resources/opensearch_security_plugin_config.md @@ -0,0 +1,79 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_opensearch_security_plugin_config Resource - terraform-provider-aiven" +subcategory: "" +description: |- + The OpenSearch Security Plugin Config resource allows the creation and management of AivenOpenSearch Security Plugin config. +--- + +# aiven_opensearch_security_plugin_config (Resource) + +The OpenSearch Security Plugin Config resource allows the creation and management of AivenOpenSearch Security Plugin config. + +## Example Usage + +```terraform +data "aiven_project" "foo" { + project = "example_project" +} + +resource "aiven_opensearch" "bar" { + project = data.aiven_project.foo.project + cloud_name = "google-europe-west1" + plan = "startup-4" + service_name = "example_service_name" + maintenance_window_dow = "monday" + maintenance_window_time = "10:00:00" +} + +resource "aiven_opensearch_user" "foo" { + service_name = aiven_opensearch.bar.service_name + project = data.aiven_project.foo.project + username = "user-example" +} + +resource "aiven_opensearch_security_config" "foo" { + project = data.aiven_project.foo.project + service_name = aiven_opensearch.bar.service_name + admin_password = "ThisIsATest123^=^" +} +``` + + +## Schema + +### Required + +- `admin_password` (String, Sensitive) The password for the os-sec-admin user. +- `project` (String) Identifies the project this resource belongs to. To set up proper dependencies please refer to this variable as a reference. This property cannot be changed, doing so forces recreation of the resource. +- `service_name` (String) Specifies the name of the service that this resource belongs to. To set up proper dependencies please refer to this variable as a reference. This property cannot be changed, doing so forces recreation of the resource. + +### Optional + +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `admin_enabled` (Boolean) Whether the os-sec-admin user is enabled. This indicates whether the user management with the security plugin is enabled. This is always true when the os-sec-admin password was set at least once. +- `available` (Boolean) Whether the security plugin is available. This is always true for recently created services. +- `enabled` (Boolean) Whether the security plugin is enabled. This is always true for recently created services. +- `id` (String) The ID of this resource. + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `default` (String) +- `delete` (String) +- `read` (String) +- `update` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import aiven_opensearch_security_plugin_config.foo project/service_name +``` diff --git a/examples/data-sources/aiven_opensearch_security_plugin_config/data-source.tf b/examples/data-sources/aiven_opensearch_security_plugin_config/data-source.tf new file mode 100644 index 000000000..6be16343f --- /dev/null +++ b/examples/data-sources/aiven_opensearch_security_plugin_config/data-source.tf @@ -0,0 +1,4 @@ +data "aiven_opensearch_security_plugin_config" "os-sec-config" { + project = aiven_project.os-project.project + service_name = aiven_opensearch.os.service_name +} diff --git a/examples/resources/aiven_opensearch_acl_config/resource.tf b/examples/resources/aiven_opensearch_acl_config/resource.tf index db7ba8bfe..8f3164148 100644 --- a/examples/resources/aiven_opensearch_acl_config/resource.tf +++ b/examples/resources/aiven_opensearch_acl_config/resource.tf @@ -1,5 +1,5 @@ data "aiven_project" "foo" { - project = "example_project" + project = "example_project" } resource "aiven_opensearch" "bar" { diff --git a/examples/resources/aiven_opensearch_security_plugin_config/import.sh b/examples/resources/aiven_opensearch_security_plugin_config/import.sh new file mode 100644 index 000000000..bdef91b49 --- /dev/null +++ b/examples/resources/aiven_opensearch_security_plugin_config/import.sh @@ -0,0 +1 @@ +terraform import aiven_opensearch_security_plugin_config.foo project/service_name diff --git a/examples/resources/aiven_opensearch_security_plugin_config/resource.tf b/examples/resources/aiven_opensearch_security_plugin_config/resource.tf new file mode 100644 index 000000000..52e8728cc --- /dev/null +++ b/examples/resources/aiven_opensearch_security_plugin_config/resource.tf @@ -0,0 +1,24 @@ +data "aiven_project" "foo" { + project = "example_project" +} + +resource "aiven_opensearch" "bar" { + project = data.aiven_project.foo.project + cloud_name = "google-europe-west1" + plan = "startup-4" + service_name = "example_service_name" + maintenance_window_dow = "monday" + maintenance_window_time = "10:00:00" +} + +resource "aiven_opensearch_user" "foo" { + service_name = aiven_opensearch.bar.service_name + project = data.aiven_project.foo.project + username = "user-example" +} + +resource "aiven_opensearch_security_config" "foo" { + project = data.aiven_project.foo.project + service_name = aiven_opensearch.bar.service_name + admin_password = "ThisIsATest123^=^" +} diff --git a/internal/schemautil/helpers.go b/internal/schemautil/helpers.go index 21bb0a912..661b2bb3d 100644 --- a/internal/schemautil/helpers.go +++ b/internal/schemautil/helpers.go @@ -7,6 +7,7 @@ import ( "github.com/aiven/aiven-go-client" "github.com/docker/go-units" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" ) // ResourceStateOrResourceDiff either *schema.ResourceState or *schema.ResourceDiff @@ -178,3 +179,16 @@ func DetermineMixedOrganizationConstraintIDToStore( return r.Account.OrganizationId, nil } + +// StringToDiagWarning is a function that converts a string to a diag warning. +func StringToDiagWarning(msg string) diag.Diagnostics { + return diag.Diagnostics{{ + Severity: diag.Warning, + Summary: msg, + }} +} + +// ErrorToDiagWarning is a function that converts an error to a diag warning. +func ErrorToDiagWarning(err error) diag.Diagnostics { + return StringToDiagWarning(err.Error()) +} diff --git a/internal/sdkprovider/provider/provider.go b/internal/sdkprovider/provider/provider.go index e1a642f9e..1628932b3 100644 --- a/internal/sdkprovider/provider/provider.go +++ b/internal/sdkprovider/provider/provider.go @@ -118,10 +118,11 @@ func Provider(version string) *schema.Provider { "aiven_flink_application_version": flink.DatasourceFlinkApplicationVersion(), // opensearch - "aiven_opensearch": opensearch.DatasourceOpenSearch(), - "aiven_opensearch_user": opensearch.DatasourceOpenSearchUser(), - "aiven_opensearch_acl_config": opensearch.DatasourceOpenSearchACLConfig(), - "aiven_opensearch_acl_rule": opensearch.DatasourceOpenSearchACLRule(), + "aiven_opensearch": opensearch.DatasourceOpenSearch(), + "aiven_opensearch_user": opensearch.DatasourceOpenSearchUser(), + "aiven_opensearch_acl_config": opensearch.DatasourceOpenSearchACLConfig(), + "aiven_opensearch_acl_rule": opensearch.DatasourceOpenSearchACLRule(), + "aiven_opensearch_security_plugin_config": opensearch.DatasourceOpenSearchSecurityPluginConfig(), // kafka "aiven_kafka": kafka.DatasourceKafka(), @@ -217,10 +218,11 @@ func Provider(version string) *schema.Provider { "aiven_flink_application_deployment": flink.ResourceFlinkApplicationDeployment(), // opensearch - "aiven_opensearch": opensearch.ResourceOpenSearch(), - "aiven_opensearch_user": opensearch.ResourceOpenSearchUser(), - "aiven_opensearch_acl_config": opensearch.ResourceOpenSearchACLConfig(), - "aiven_opensearch_acl_rule": opensearch.ResourceOpenSearchACLRule(), + "aiven_opensearch": opensearch.ResourceOpenSearch(), + "aiven_opensearch_user": opensearch.ResourceOpenSearchUser(), + "aiven_opensearch_acl_config": opensearch.ResourceOpenSearchACLConfig(), + "aiven_opensearch_acl_rule": opensearch.ResourceOpenSearchACLRule(), + "aiven_opensearch_security_plugin_config": opensearch.ResourceOpenSearchSecurityPluginConfig(), // kafka "aiven_kafka": kafka.ResourceKafka(), diff --git a/internal/sdkprovider/service/opensearch/opensearch_acl_config.go b/internal/sdkprovider/service/opensearch/opensearch_acl_config.go index 174732aad..d532b712a 100644 --- a/internal/sdkprovider/service/opensearch/opensearch_acl_config.go +++ b/internal/sdkprovider/service/opensearch/opensearch_acl_config.go @@ -1,3 +1,4 @@ +// Package opensearch implements the Aiven OpenSearch service. package opensearch import ( @@ -30,7 +31,7 @@ var aivenOpenSearchACLConfigSchema = map[string]*schema.Schema{ func ResourceOpenSearchACLConfig() *schema.Resource { return &schema.Resource{ - Description: "The OpenSearch resource allows the creation and management of Aiven OpenSearch services.", + Description: "The OpenSearch ACL Config resource allows the creation and management of Aiven OpenSearch ACLs.", CreateContext: resourceOpenSearchACLConfigUpdate, ReadContext: resourceOpenSearchACLConfigRead, UpdateContext: resourceOpenSearchACLConfigUpdate, diff --git a/internal/sdkprovider/service/opensearch/opensearch_security_plugin_config.go b/internal/sdkprovider/service/opensearch/opensearch_security_plugin_config.go new file mode 100644 index 000000000..e1723b4a9 --- /dev/null +++ b/internal/sdkprovider/service/opensearch/opensearch_security_plugin_config.go @@ -0,0 +1,170 @@ +// Package opensearch implements the Aiven OpenSearch service. +package opensearch + +import ( + "context" + + "github.com/aiven/aiven-go-client" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +// aivenOpenSearchSecurityPluginConfigSchema holds the schema for the OpenSearch Security Plugin Config resource. +var aivenOpenSearchSecurityPluginConfigSchema = map[string]*schema.Schema{ + "project": schemautil.CommonSchemaProjectReference, + "service_name": schemautil.CommonSchemaServiceNameReference, + "admin_password": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "The password for the os-sec-admin user.", + }, + "available": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether the security plugin is available. This is always true for recently created services.", + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether the security plugin is enabled. This is always true for recently created services.", + }, + "admin_enabled": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether the os-sec-admin user is enabled. This indicates whether the user management with the" + + " security plugin is enabled. This is always true when the os-sec-admin password was set at least once.", + }, +} + +// ResourceOpenSearchSecurityPluginConfig defines the OpenSearch Security Plugin Config resource. +func ResourceOpenSearchSecurityPluginConfig() *schema.Resource { + return &schema.Resource{ + Description: "The OpenSearch Security Plugin Config resource allows the creation and management of Aiven" + + "OpenSearch Security Plugin config.", + CreateContext: resourceOpenSearchSecurityPluginConfigCreate, + ReadContext: resourceOpenSearchSecurityPluginConfigRead, + UpdateContext: resourceOpenSearchSecurityPluginConfigUpdate, + DeleteContext: resourceOpenSearchSecurityPluginConfigDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: schemautil.DefaultResourceTimeouts(), + Schema: aivenOpenSearchSecurityPluginConfigSchema, + } +} + +// resourceOpenSearchSecurityPluginConfigCreate applies an OpenSearch Security Plugin config to an existing OpenSearch +// service, enabling the OpenSearch Security Plugin. +func resourceOpenSearchSecurityPluginConfigCreate( + ctx context.Context, + d *schema.ResourceData, + m any, +) diag.Diagnostics { + client := m.(*aiven.Client) + + project := d.Get("project").(string) + + serviceName := d.Get("service_name").(string) + + if _, err := client.OpenSearchSecurityPluginHandler.Enable( + project, + serviceName, + aiven.OpenSearchSecurityPluginEnableRequest{ + AdminPassword: d.Get("admin_password").(string), + }, + ); err != nil { + return diag.FromErr(err) + } + + d.SetId(schemautil.BuildResourceID(project, serviceName)) + + return resourceOpenSearchSecurityPluginConfigRead(ctx, d, m) +} + +// resourceOpenSearchSecurityPluginConfigRead reads the OpenSearch Security Plugin config from an existing OpenSearch +// service. +func resourceOpenSearchSecurityPluginConfigRead( + _ context.Context, + d *schema.ResourceData, + m any, +) diag.Diagnostics { + client := m.(*aiven.Client) + + project, serviceName, err := schemautil.SplitResourceID2(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + r, err := client.OpenSearchSecurityPluginHandler.Get(project, serviceName) + if err != nil { + return diag.FromErr(schemautil.ResourceReadHandleNotFound(err, d)) + } + + if err := d.Set("project", project); err != nil { + return diag.Errorf("error setting `project` for resource %s: %s", d.Id(), err) + } + + if err := d.Set("service_name", serviceName); err != nil { + return diag.Errorf("error setting `service_name` for resource %s: %s", d.Id(), err) + } + + if err := d.Set("available", r.SecurityPluginAvailable); err != nil { + return diag.Errorf("error setting `available` for resource %s: %s", d.Id(), err) + } + + if err := d.Set("enabled", r.SecurityPluginEnabled); err != nil { + return diag.Errorf("error setting `enabled` for resource %s: %s", d.Id(), err) + } + + if err := d.Set("admin_enabled", r.SecurityPluginAdminEnabled); err != nil { + return diag.Errorf("error setting `admin_enabled` for resource %s: %s", d.Id(), err) + } + + return nil +} + +// resourceOpenSearchSecurityPluginConfigUpdate updates the OpenSearch Security Plugin config on an existing OpenSearch +// service. +func resourceOpenSearchSecurityPluginConfigUpdate( + ctx context.Context, + d *schema.ResourceData, + m any, +) diag.Diagnostics { + client := m.(*aiven.Client) + + project := d.Get("project").(string) + + serviceName := d.Get("service_name").(string) + + oldAdminPassword, newAdminPassword := d.GetChange("admin_password") + + if _, err := client.OpenSearchSecurityPluginHandler.UpdatePassword( + project, + serviceName, + aiven.OpenSearchSecurityPluginUpdatePasswordRequest{ + AdminPassword: oldAdminPassword.(string), + NewPassword: newAdminPassword.(string), + }, + ); err != nil { + return diag.FromErr(err) + } + + d.SetId(schemautil.BuildResourceID(project, serviceName)) + + return resourceOpenSearchSecurityPluginConfigRead(ctx, d, m) +} + +// resourceOpenSearchSecurityPluginConfigDelete disables the OpenSearch Security Plugin on an existing OpenSearch +// service. +// +// Currently, the Aiven API does not support disabling the OpenSearch Security Plugin, so this resource is effectively +// a no-op. Additionally, we display a warning to the user to indicate that the OpenSearch Security Plugin is not +// disabled, but the resource is still going to be deleted. +func resourceOpenSearchSecurityPluginConfigDelete(_ context.Context, _ *schema.ResourceData, _ any) diag.Diagnostics { + return schemautil.StringToDiagWarning("It is not possible to disable the OpenSearch Security Plugin once " + + "it has been enabled. This resource is going to be deleted, but the OpenSearch Security Plugin will remain " + + "enabled.") +} diff --git a/internal/sdkprovider/service/opensearch/opensearch_security_plugin_config_data_source.go b/internal/sdkprovider/service/opensearch/opensearch_security_plugin_config_data_source.go new file mode 100644 index 000000000..8e250defa --- /dev/null +++ b/internal/sdkprovider/service/opensearch/opensearch_security_plugin_config_data_source.go @@ -0,0 +1,42 @@ +// Package opensearch implements the Aiven OpenSearch service. +package opensearch + +import ( + "context" + + "github.com/aiven/aiven-go-client" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +// DatasourceOpenSearchSecurityPluginConfig defines the OpenSearch Security Plugin Config data source. +func DatasourceOpenSearchSecurityPluginConfig() *schema.Resource { + return &schema.Resource{ + ReadContext: datasourceOpenSearchSecurityPluginConfigRead, + Description: "The OpenSearch Security Plugin Config data source provides information about an existing Aiven" + + " OpenSearch Security Plugin Config.", + Schema: schemautil.ResourceSchemaAsDatasourceSchema( + aivenOpenSearchSecurityPluginConfigSchema, "project", "service_name", + ), + } +} + +// datasourceOpenSearchSecurityPluginConfigRead reads the configuration of an existing OpenSearch Security Plugin +// Config. +func datasourceOpenSearchSecurityPluginConfigRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client := m.(*aiven.Client) + + projectName := d.Get("project").(string) + + serviceName := d.Get("service_name").(string) + + if _, err := client.OpenSearchSecurityPluginHandler.Get(projectName, serviceName); err != nil { + return diag.FromErr(err) + } + + d.SetId(schemautil.BuildResourceID(projectName, serviceName)) + + return resourceOpenSearchSecurityPluginConfigRead(ctx, d, m) +} diff --git a/internal/sdkprovider/service/opensearch/opensearch_security_plugin_config_test.go b/internal/sdkprovider/service/opensearch/opensearch_security_plugin_config_test.go new file mode 100644 index 000000000..2c3b58d95 --- /dev/null +++ b/internal/sdkprovider/service/opensearch/opensearch_security_plugin_config_test.go @@ -0,0 +1,137 @@ +// Package opensearch_test implements tests for the Aiven OpenSearch service. +package opensearch_test + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + acc "github.com/aiven/terraform-provider-aiven/internal/acctest" +) + +// openSearchSecurityPluginTestPassword is the password used for the OpenSearch Security Plugin Config tests. +const openSearchSecurityPluginTestPassword = "ThisIsATest123^=^" + +// TestAccAivenOpenSearchSecurityPluginConfig_basic tests the basic functionality of the OpenSearch Security Plugin +// Config resource. +func TestAccAivenOpenSearchSecurityPluginConfig_basic(t *testing.T) { + resourceName := "aiven_opensearch_security_plugin_config.foo" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +data "aiven_project" "foo" { + project = "%s" +} + +resource "aiven_opensearch" "bar" { + project = data.aiven_project.foo.project + cloud_name = "google-europe-west1" + plan = "startup-4" + service_name = "test-acc-sr-os-sec-plugin-%[2]s" + maintenance_window_dow = "monday" + maintenance_window_time = "10:00:00" +} + +resource "aiven_opensearch_user" "foo" { + service_name = aiven_opensearch.bar.service_name + project = data.aiven_project.foo.project + username = "user-%[2]s" +} + +resource "aiven_opensearch_security_plugin_config" "foo" { + project = data.aiven_project.foo.project + service_name = aiven_opensearch.bar.service_name + admin_password = "%s" + + depends_on = [aiven_opensearch.bar, aiven_opensearch_user.foo] +}`, os.Getenv("AIVEN_PROJECT_NAME"), rName, openSearchSecurityPluginTestPassword), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr( + resourceName, "service_name", fmt.Sprintf("test-acc-sr-os-sec-plugin-%s", rName), + ), + resource.TestCheckResourceAttr( + resourceName, "admin_password", openSearchSecurityPluginTestPassword, + ), + resource.TestCheckResourceAttr(resourceName, "available", "true"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "admin_enabled", "true"), + ), + }, + { + Config: fmt.Sprintf(` +data "aiven_project" "foo" { + project = "%s" +} + +resource "aiven_opensearch" "bar" { + project = data.aiven_project.foo.project + cloud_name = "google-europe-west1" + plan = "startup-4" + service_name = "test-acc-sr-os-sec-plugin-%[2]s" + maintenance_window_dow = "monday" + maintenance_window_time = "10:00:00" +} + +resource "aiven_opensearch_security_plugin_config" "foo" { + project = data.aiven_project.foo.project + service_name = aiven_opensearch.bar.service_name + admin_password = "%s" + + depends_on = [aiven_opensearch.bar] +}`, os.Getenv("AIVEN_PROJECT_NAME"), rName, openSearchSecurityPluginTestPassword), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr( + resourceName, "service_name", fmt.Sprintf("test-acc-sr-os-sec-plugin-%s", rName), + ), + resource.TestCheckResourceAttr( + resourceName, "admin_password", openSearchSecurityPluginTestPassword, + ), + resource.TestCheckResourceAttr(resourceName, "available", "true"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "admin_enabled", "true"), + ), + }, + { + Config: fmt.Sprintf(` +data "aiven_project" "foo" { + project = "%s" +} + +resource "aiven_opensearch" "bar" { + project = data.aiven_project.foo.project + cloud_name = "google-europe-west1" + plan = "startup-4" + service_name = "test-acc-sr-os-sec-plugin-%[2]s" + maintenance_window_dow = "monday" + maintenance_window_time = "10:00:00" +} + +resource "aiven_opensearch_user" "foo" { + service_name = aiven_opensearch.bar.service_name + project = data.aiven_project.foo.project + username = "user-%[2]s" +} + +resource "aiven_opensearch_security_plugin_config" "foo" { + project = data.aiven_project.foo.project + service_name = aiven_opensearch.bar.service_name + admin_password = "%s" + + depends_on = [aiven_opensearch.bar, aiven_opensearch_user.foo] +}`, os.Getenv("AIVEN_PROJECT_NAME"), rName, openSearchSecurityPluginTestPassword), + ExpectError: regexp.MustCompile("when the OpenSearch Security Plugin is enabled"), + }, + }, + }) +} diff --git a/internal/sdkprovider/service/opensearch/opensearch_user.go b/internal/sdkprovider/service/opensearch/opensearch_user.go index 147f42768..b2998b608 100644 --- a/internal/sdkprovider/service/opensearch/opensearch_user.go +++ b/internal/sdkprovider/service/opensearch/opensearch_user.go @@ -1,12 +1,24 @@ +// Package opensearch implements the Aiven OpenSearch service. package opensearch import ( + "context" + "errors" + "strings" + + "github.com/aiven/aiven-go-client" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/aiven/terraform-provider-aiven/internal/schemautil" "github.com/aiven/terraform-provider-aiven/internal/schemautil/userconfig" ) +// errOpenSearchConfiguredDirectly is an error that we expect to be returned by the Aiven client when the OpenSearch +// Security Plugin is enabled for the OpenSearch service, and the user is trying to manage the OpenSearch users via +// the Aiven API. +const errOpenSearchConfiguredDirectly = "access to service is configured directly by opensearch security" + var aivenOpenSearchUserSchema = map[string]*schema.Schema{ "project": schemautil.CommonSchemaProjectReference, "service_name": schemautil.CommonSchemaServiceNameReference, @@ -38,10 +50,10 @@ var aivenOpenSearchUserSchema = map[string]*schema.Schema{ func ResourceOpenSearchUser() *schema.Resource { return &schema.Resource{ Description: "The OpenSearch User resource allows the creation and management of Aiven OpenSearch Users.", - CreateContext: schemautil.ResourceServiceUserCreate, - UpdateContext: schemautil.ResourceServiceUserUpdate, - ReadContext: schemautil.ResourceServiceUserRead, - DeleteContext: schemautil.ResourceServiceUserDelete, + CreateContext: resourceOpenSearchUserCreate, + ReadContext: resourceOpenSearchUserRead, + UpdateContext: resourceOpenSearchUserUpdate, + DeleteContext: resourceOpenSearchUserDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -50,3 +62,96 @@ func ResourceOpenSearchUser() *schema.Resource { Schema: aivenOpenSearchUserSchema, } } + +// detourSecurityPluginEnabledCheck checks if the OpenSearch Security Plugin is enabled for the OpenSearch service. +// If it is enabled, it returns an error, and the resource is not allowed to be created, read or updated. +func detourSecurityPluginEnabledCheck(d *schema.ResourceData, m any) error { + client := m.(*aiven.Client) + + project := d.Get("project").(string) + serviceName := d.Get("service_name").(string) + + r, err := client.OpenSearchSecurityPluginHandler.Get(project, serviceName) + if err == nil && r.SecurityPluginAdminEnabled { + return errors.New("when the OpenSearch Security Plugin is enabled, OpenSearch users are being " + + "managed by it; delete the aiven_opensearch_user resource(s), and manage the users via the " + + "OpenSearch Security Plugin instead; any changes to the aiven_opensearch_user resource(s) are not " + + "going to have any effect now") + } + + return err +} + +// resourceOpenSearchUserCreate creates a OpenSearch User. +func resourceOpenSearchUserCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := detourSecurityPluginEnabledCheck(d, m); err != nil { + return diag.FromErr(err) + } + + return schemautil.ResourceServiceUserCreate(ctx, d, m) +} + +// resourceOpenSearchUserRead reads a OpenSearch User into the Terraform state. +func resourceOpenSearchUserRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + diags := schemautil.ResourceServiceUserRead(ctx, d, m) + + if diags == nil { + return nil + } + + var e *diag.Diagnostic + + for _, v := range diags { + if v.Severity == diag.Error { + if e != nil { + panic("multiple errors in diags; this should never happen") + } + + e = &v + } + } + + if err := detourSecurityPluginEnabledCheck(d, m); err != nil && + strings.Contains(strings.ToLower(e.Summary), errOpenSearchConfiguredDirectly) { + return schemautil.ErrorToDiagWarning(err) + } + + return diags +} + +// resourceOpenSearchUserUpdate updates a OpenSearch User. +func resourceOpenSearchUserUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := detourSecurityPluginEnabledCheck(d, m); err != nil { + return diag.FromErr(err) + } + + return schemautil.ResourceServiceUserUpdate(ctx, d, m) +} + +// resourceOpenSearchUserDelete deletes a OpenSearch User. +func resourceOpenSearchUserDelete(_ context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client := m.(*aiven.Client) + + projectName, serviceName, username, err := schemautil.SplitResourceID3(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + err = client.ServiceUsers.Delete(projectName, serviceName, username) + if err != nil && !aiven.IsNotFound(err) { + var e aiven.Error + + // This is a special case where the user is not managed by Aiven, but by the OpenSearch Security plugin. + // We don't want to fail on destroy operations if the OpenSearch Security Plugin is enabled, + // because the users of our provider wouldn't want to have obsolete resources in their manifests, so we + // nullify the error instead of returning it, and the resource is allowed to be destroyed, while + // performing a no-op. + if errors.As(err, &e) && strings.Contains(strings.ToLower(e.Message), errOpenSearchConfiguredDirectly) { + return nil + } + + return diag.FromErr(err) + } + + return nil +}