Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- Add `slo_id` validation to `elasticstack_kibana_slo` ([#1221](https://github.com/elastic/terraform-provider-elasticstack/pull/1221))
- Add `ignore_missing_component_templates` to `elasticstack_elasticsearch_index_template` ([#1206](https://github.com/elastic/terraform-provider-elasticstack/pull/1206))
- Prevent provider panic when a script exists in state, but not in Elasticsearch ([#1218](https://github.com/elastic/terraform-provider-elasticstack/pull/1218))
- Add support for managing cross_cluster API keys in `elasticstack_elasticsearch_security_api_key` ([#1252](https://github.com/elastic/terraform-provider-elasticstack/pull/1252))
- Allow version changes without a destroy/create cycle with `elasticstack_fleet_integration` ([#1255](https://github.com/elastic/terraform-provider-elasticstack/pull/1255)). This fixes an issue where it was impossible to upgrade integrations which are used by an integration policy.
- Add `namespace` attribute to `elasticstack_kibana_synthetics_monitor` resource to support setting data stream namespace independently from `space_id` ([#1247](https://github.com/elastic/terraform-provider-elasticstack/pull/1247))

Expand Down
65 changes: 65 additions & 0 deletions docs/resources/elasticsearch_security_api_key.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,38 @@ output "api_key" {
value = elasticstack_elasticsearch_security_api_key.api_key
sensitive = true
}

# Example: Cross-cluster API key
resource "elasticstack_elasticsearch_security_api_key" "cross_cluster_key" {
name = "My Cross-Cluster API Key"
type = "cross_cluster"

# Define access permissions for cross-cluster operations
access = {

# Grant replication access to specific indices
replication = [
{
names = ["archive-*"]
}
]
}

# Set the expiration for the API key
expiration = "30d"

# Set arbitrary metadata
metadata = jsonencode({
description = "Cross-cluster key for production environment"
environment = "production"
team = "platform"
})
}

output "cross_cluster_api_key" {
value = elasticstack_elasticsearch_security_api_key.cross_cluster_key
sensitive = true
}
```

<!-- schema generated by tfplugindocs -->
Expand All @@ -87,10 +119,12 @@ output "api_key" {

### Optional

- `access` (Attributes) Access configuration for cross-cluster API keys. Only applicable when type is 'cross_cluster'. (see [below for nested schema](#nestedatt--access))
- `elasticsearch_connection` (Block List, Deprecated) Elasticsearch connection configuration block. (see [below for nested schema](#nestedblock--elasticsearch_connection))
- `expiration` (String) Expiration time for the API key. By default, API keys never expire.
- `metadata` (String) Arbitrary metadata that you want to associate with the API key.
- `role_descriptors` (String) Role descriptors for this API key.
- `type` (String) The type of API key. Valid values are 'rest' (default) and 'cross_cluster'. Cross-cluster API keys are used for cross-cluster search and replication.

### Read-Only

Expand All @@ -100,6 +134,37 @@ output "api_key" {
- `id` (String) Internal identifier of the resource.
- `key_id` (String) Unique id for this API key.

<a id="nestedatt--access"></a>
### Nested Schema for `access`

Optional:

- `replication` (Attributes List) A list of replication configurations for which the cross-cluster API key will have replication privileges. (see [below for nested schema](#nestedatt--access--replication))
- `search` (Attributes List) A list of search configurations for which the cross-cluster API key will have search privileges. (see [below for nested schema](#nestedatt--access--search))

<a id="nestedatt--access--replication"></a>
### Nested Schema for `access.replication`

Required:

- `names` (List of String) A list of index patterns for replication.


<a id="nestedatt--access--search"></a>
### Nested Schema for `access.search`

Required:

- `names` (List of String) A list of index patterns for search.

Optional:

- `allow_restricted_indices` (Boolean) Whether to allow access to restricted indices.
- `field_security` (String) Field-level security configuration in JSON format.
- `query` (String) Query to filter documents for search operations in JSON format.



<a id="nestedblock--elasticsearch_connection"></a>
### Nested Schema for `elasticsearch_connection`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,35 @@ output "api_key" {
value = elasticstack_elasticsearch_security_api_key.api_key
sensitive = true
}

# Example: Cross-cluster API key
resource "elasticstack_elasticsearch_security_api_key" "cross_cluster_key" {
name = "My Cross-Cluster API Key"
type = "cross_cluster"

# Define access permissions for cross-cluster operations
access = {

# Grant replication access to specific indices
replication = [
{
names = ["archive-*"]
}
]
}

# Set the expiration for the API key
expiration = "30d"

# Set arbitrary metadata
metadata = jsonencode({
description = "Cross-cluster key for production environment"
environment = "production"
team = "platform"
})
}

output "cross_cluster_api_key" {
value = elasticstack_elasticsearch_security_api_key.cross_cluster_key
sensitive = true
}
55 changes: 55 additions & 0 deletions internal/clients/elasticsearch/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,3 +440,58 @@ func DeleteApiKey(apiClient *clients.ApiClient, id string) fwdiag.Diagnostics {
}
return nil
}

func CreateCrossClusterApiKey(apiClient *clients.ApiClient, apikey *models.CrossClusterApiKey) (*models.CrossClusterApiKeyCreateResponse, fwdiag.Diagnostics) {
apikeyBytes, err := json.Marshal(apikey)
if err != nil {
return nil, utils.FrameworkDiagFromError(err)
}

esClient, err := apiClient.GetESClient()
if err != nil {
return nil, utils.FrameworkDiagFromError(err)
}
res, err := esClient.Security.CreateCrossClusterAPIKey(bytes.NewReader(apikeyBytes))
if err != nil {
return nil, utils.FrameworkDiagFromError(err)
}
defer res.Body.Close()
if diags := utils.CheckError(res, "Unable to create cross cluster apikey"); diags.HasError() {
return nil, utils.FrameworkDiagsFromSDK(diags)
}

var apiKey models.CrossClusterApiKeyCreateResponse

if err := json.NewDecoder(res.Body).Decode(&apiKey); err != nil {
return nil, utils.FrameworkDiagFromError(err)
}

return &apiKey, nil
}

func UpdateCrossClusterApiKey(apiClient *clients.ApiClient, apikey models.CrossClusterApiKey) fwdiag.Diagnostics {
id := apikey.ID

apikey.Expiration = ""
apikey.Name = ""
apikey.ID = ""
apikeyBytes, err := json.Marshal(apikey)
if err != nil {
return utils.FrameworkDiagFromError(err)
}

esClient, err := apiClient.GetESClient()
if err != nil {
return utils.FrameworkDiagFromError(err)
}
res, err := esClient.Security.UpdateCrossClusterAPIKey(id, bytes.NewReader(apikeyBytes))
if err != nil {
return utils.FrameworkDiagFromError(err)
}
defer res.Body.Close()
if diags := utils.CheckError(res, "Unable to update cross cluster apikey"); diags.HasError() {
return utils.FrameworkDiagsFromSDK(diags)
}

return nil
}
101 changes: 101 additions & 0 deletions internal/elasticsearch/security/api_key/acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,3 +450,104 @@ func checkResourceSecurityApiKeyDestroy(s *terraform.State) error {
}
return nil
}

func TestAccResourceSecurityApiKeyCrossCluster(t *testing.T) {
// generate a random name
apiKeyName := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum)

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
CheckDestroy: checkResourceSecurityApiKeyDestroy,
ProtoV6ProviderFactories: acctest.Providers,
Steps: []resource.TestStep{
{
SkipFunc: versionutils.CheckIfVersionIsUnsupported(api_key.MinVersionWithCrossCluster),
Config: testAccResourceSecurityApiKeyCrossClusterCreate(apiKeyName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "name", apiKeyName),
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "type", "cross_cluster"),
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "access.search.0.names.0", "logs-*"),
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "access.search.0.names.1", "metrics-*"),
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "access.replication.0.names.0", "archive-*"),
),
},
{
SkipFunc: versionutils.CheckIfVersionIsUnsupported(api_key.MinVersionWithCrossCluster),
Config: testAccResourceSecurityApiKeyCrossClusterUpdate(apiKeyName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "name", apiKeyName),
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "type", "cross_cluster"),
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "access.search.0.names.0", "log-*"),
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "access.search.0.names.1", "metrics-*"),
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "access.replication.0.names.0", "archives-*"),
),
},
},
})
}

func testAccResourceSecurityApiKeyCrossClusterCreate(apiKeyName string) string {
return fmt.Sprintf(`
provider "elasticstack" {
elasticsearch {}
}

resource "elasticstack_elasticsearch_security_api_key" "test" {
name = "%s"
type = "cross_cluster"

access = {
search = [
{
names = ["logs-*", "metrics-*"]
}
]
replication = [
{
names = ["archive-*"]
}
]
}

expiration = "30d"

metadata = jsonencode({
description = "Cross-cluster test key"
environment = "test"
})
}
`, apiKeyName)
}

func testAccResourceSecurityApiKeyCrossClusterUpdate(apiKeyName string) string {
return fmt.Sprintf(`
provider "elasticstack" {
elasticsearch {}
}

resource "elasticstack_elasticsearch_security_api_key" "test" {
name = "%s"
type = "cross_cluster"

access = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to have this acceptance test update something in access to get some IT style acceptance test coverage of SetUnknownIfAccessHasChanges?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, it does exactly that.

search = [
{
names = ["log-*", "metrics-*"]
}
]
replication = [
{
names = ["archives-*"]
}
]
}

expiration = "30d"

metadata = jsonencode({
description = "Cross-cluster test key"
environment = "test"
})
}
`, apiKeyName)
}
Loading
Loading