Compare commits

...
6 Commits
Author SHA1 Message Date
Elim Tsiagbey 008a6ff618 chore: Create changelog for v1.1.9 (#434)
Release / go-version (push) Successful in 4s
Release / release-notes (push) Successful in 5s
Release / Terraform Provider Release (push) Failing after 0s
Target for release on July 19, 2023
2023-07-19 10:53:40 -04:00
Elim Tsiagbey 8983decd06 fix: Append correct storage bucket worker filter option (#433)
During terraform update, the default worker filter option used is the `DefaultBucketName` instead of `DefaultWorkerFilter`. Update it to use the correct option
2023-07-18 14:29:03 -04:00
Elim Tsiagbey 605d124d51 feat: add target resource session recording support (#421)
* feat: add target session recording support

- add new optional `enable_session_recording` field for enabling or disabling session recording
- add new optional `storage_bucket_id` field for setting the storage bucket for the target
- These new fields are applicable to Boundary HCP/ENT

### Considerations
- There is no test coverage yet until we add support for testing enterprise only features. Manually tested the changes
2023-06-30 13:11:15 -04:00
Elim Tsiagbey 9186304d5d feat: add storage bucket resource (#417)
* feat: add storage bucket resource

- Create a new resource for storage bucket
- Add examples and docs for how to use new resource
- Tests added but they do not run on purpose and have not been validated yet because we cannot run tests for enterprise features yet. That will be addressed separately

## Considerations
- Even though only AWS can be supported today, still added the `pluginID` & `pluginName` fields so it's easy to extend once other plugin types are supported
- There is some logic around handling secrets sent to Boundary and validating secret HMAC changes that exist from `boundary_host_catalog_plugin` that was introduced in [PR-159](https://github.com/hashicorp/terraform-provider-boundary/pull/159). Used the same logic we've been using for that
2023-06-28 10:38:22 -04:00
Elim Tsiagbey 1c05eab410 docs: update host set plugin filters examples (#420)
This change is to update the example docs for `host_set_plugin` filters attribute to use a list strings instead of just strings.

WIthout using a list of strings, Terraform will falsely detect diffs with host set plugin `filters`.


AWS host set plugin expects filters attributes to be passed as a list of strings instead of strings. It is currently backwards compatible with strings due to the migration [normalizeData func](https://github.com/hashicorp/boundary-plugin-aws/blob/7bf9fe2ecf1f20d7ff8aea8306909abcde1d17a4/plugin/service/host/plugin.go#L256) on AWS host plugin.

Previously this was not working but [Boundary PR-3338](https://github.com/hashicorp/boundary/pull/3338) fixed the bug.
2023-06-23 14:21:21 -04:00
Louis Ruch 749cce8ec1 chore: prep for release 2023-06-12 21:48:13 -07:00
13 changed files with 1182 additions and 11 deletions

No files matched your search

+11
View File
@@ -2,6 +2,17 @@
Canonical reference for changes, improvements, and bugfixes for the Boundary Terraform provider.
## 1.1.9 (July 19, 2023)
### New and Improved
* Add support for a storage bucket as a resource
([PR](https://github.com/hashicorp/terraform-provider-boundary/pull/417))
* Add option to enable session recording on a target resource
([PR](https://github.com/hashicorp/terraform-provider-boundary/pull/421))
* Update docs for host set plugin filters examples
([PR](https://github.com/hashicorp/terraform-provider-boundary/pull/420))
## 1.1.8 (June 13, 2023)
### New and Improved
+3 -5
View File
@@ -51,7 +51,7 @@ resource "boundary_host_catalog_plugin" "aws_example" {
resource "boundary_host_set_plugin" "web" {
name = "My web host set plugin"
host_catalog_id = boundary_host_catalog_plugin.aws_example.id
attributes_json = jsonencode({ "filters" = "tag:service-type=web" })
attributes_json = jsonencode({ "filters" = ["tag:service-type=web"] })
}
resource "boundary_host_set_plugin" "foobar" {
@@ -59,8 +59,7 @@ resource "boundary_host_set_plugin" "foobar" {
host_catalog_id = boundary_host_catalog_plugin.aws_example.id
preferred_endpoints = ["cidr:54.0.0.0/8"]
attributes_json = jsonencode({
"filters" = "tag-key=foo",
"filters" = "tag-key=bar"
"filters" = ["tag-key=foo", "tag-key=bar"]
})
}
@@ -69,8 +68,7 @@ resource "boundary_host_set_plugin" "launch" {
host_catalog_id = boundary_host_catalog_plugin.aws_example.id
sync_interval_seconds = 60
attributes_json = jsonencode({
"filters" = "tag:development=prod,dev",
"filters" = "launch-time=2022-01-04T*"
"filters" = ["tag:development=prod,dev", "launch-time=2022-01-04T*"]
})
}
+75
View File
@@ -0,0 +1,75 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "boundary_storage_bucket Resource - terraform-provider-boundary"
subcategory: ""
description: |-
The storage bucket resource allows you to configure a Boundary storage bucket. A storage bucket can only belong to the Global scope or an Org scope. At this time, the only supported storage for storage buckets is AWS S3. This feature requires Boundary Enterprise or Boundary HCP.
---
# boundary_storage_bucket (Resource)
The storage bucket resource allows you to configure a Boundary storage bucket. A storage bucket can only belong to the Global scope or an Org scope. At this time, the only supported storage for storage buckets is AWS S3. This feature requires Boundary Enterprise or Boundary HCP.
## Example Usage
```terraform
resource "boundary_scope" "org" {
name = "organization_one"
description = "My first scope!"
scope_id = boundary_scope.global.id
auto_create_admin_role = true
auto_create_default_role = true
}
resource "boundary_storage_bucket" "aws_example" {
name = "My aws storage bucket"
description = "My first storage bucket!"
scope_id = boundary_scope.org.id
plugin_name = "aws"
bucket_name = "mybucket"
attributes_json = jsonencode({ "region" = "us-east-1" })
# recommended to pass in aws secrets using a file() or using environment variables
# the secrets below must be generated in aws by creating a aws iam user with programmatic access
secrets_json = jsonencode({
"access_key_id" = "aws_access_key_id_value",
"secret_access_key" = "aws_secret_access_key_value"
})
worker_filter = "\"pki\" in \"/tags/type\""
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `bucket_name` (String) The name of the bucket within the external object store service.
- `scope_id` (String) The scope for this storage bucket.
- `secrets_json` (String, Sensitive) The secrets for the storage bucket. Either values encoded with the "jsonencode" function, pre-escaped JSON string, or a file:// or env:// path. Set to a string "null" to clear any existing values. NOTE: Unlike "attributes_json", removing this block will NOT clear secrets from the storage bucket; this allows injecting secrets for one call, then removing them for storage.
- `worker_filter` (String) Filters to the worker(s) that can handle requests for this storage bucket. The filter must match an existing worker in order to create a storage bucket.
### Optional
- `attributes_json` (String) The attributes for the storage bucket. The "region" attribute field is required when creating an AWS storage bucket. Values are either encoded with the "jsonencode" function, pre-escaped JSON string, or a file:// or env:// path. Set to a string "null" or remove the block to clear all attributes in the storage bucket.
- `bucket_prefix` (String) The prefix used to organize the data held within the external object store.
- `description` (String) The storage bucket description.
- `name` (String) The storage bucket name. Defaults to the resource name.
- `plugin_id` (String) The ID of the plugin that should back the resource. This or plugin_name must be defined.
- `plugin_name` (String) The name of the plugin that should back the resource. This or plugin_id must be defined.
### Read-Only
- `id` (String) The ID of the storage bucket.
- `internal_force_update` (String) Internal only. Used to force update so that we can always check the value of secrets.
- `internal_hmac_used_for_secrets_config_hmac` (String) Internal only. The Boundary-provided HMAC used to calculate the current value of the HMAC'd config. Used for drift detection.
- `internal_secrets_config_hmac` (String) Internal only. HMAC of (serverSecretsHmac + config secrets). Used for proper secrets handling.
- `secrets_hmac` (String) The HMAC'd secrets value returned from the server.
## Import
Import is supported using the following syntax:
```shell
terraform import boundary_storage_bucket.foo <my-id>
```
+32
View File
@@ -84,6 +84,20 @@ resource "boundary_host_set" "foo" {
]
}
resource "boundary_storage_bucket" "aws_example" {
name = "My aws storage bucket"
description = "My first storage bucket!"
scope_id = boundary_scope.org.id
plugin_name = "aws"
bucket_name = "mybucket"
attributes_json = jsonencode({ "region" = "us-east-1" })
secrets_json = jsonencode({
"access_key_id" = "aws_access_key_id_value",
"secret_access_key" = "aws_secret_access_key_value"
})
worker_filter = "\"pki\" in \"/tags/type\""
}
resource "boundary_target" "foo" {
name = "foo"
description = "Foo target"
@@ -112,6 +126,22 @@ resource "boundary_target" "ssh_foo" {
]
}
resource "boundary_target" "ssh_session_recording_foo" {
name = "ssh_foo"
description = "Ssh target"
type = "ssh"
default_port = "22"
scope_id = boundary_scope.project.id
host_source_ids = [
boundary_host_set.foo.id
]
injected_application_credential_source_ids = [
boundary_credential_library_vault.foo.id
]
enable_session_recording = true
storage_bucket_id = boundary_storage_bucket.aws_example
}
resource "boundary_target" "address_foo" {
name = "address_foo"
description = "Foo target with an address"
@@ -138,12 +168,14 @@ resource "boundary_target" "address_foo" {
- `default_port` (Number) The default port for this target.
- `description` (String) The target description.
- `egress_worker_filter` (String) Boolean expression to filter the workers used to access this target
- `enable_session_recording` (Boolean) HCP/Ent Only. Enable sessions recording for this target. Only applicable for SSH targets.
- `host_source_ids` (Set of String) A list of host source ID's. Cannot be used alongside address.
- `ingress_worker_filter` (String) HCP Only. Boolean expression to filter the workers a user will connect to when initiating a session against this target
- `injected_application_credential_source_ids` (Set of String) A list of injected application credential source ID's.
- `name` (String) The target name. Defaults to the resource name.
- `session_connection_limit` (Number)
- `session_max_seconds` (Number)
- `storage_bucket_id` (String) HCP/Ent Only. Storage bucket for this target. Only applicable for SSH targets.
- `worker_filter` (String, Deprecated) Boolean expression to filter the workers for this target
### Read-Only
@@ -36,7 +36,7 @@ resource "boundary_host_catalog_plugin" "aws_example" {
resource "boundary_host_set_plugin" "web" {
name = "My web host set plugin"
host_catalog_id = boundary_host_catalog_plugin.aws_example.id
attributes_json = jsonencode({ "filters" = "tag:service-type=web" })
attributes_json = jsonencode({ "filters" = ["tag:service-type=web"] })
}
resource "boundary_host_set_plugin" "foobar" {
@@ -44,8 +44,7 @@ resource "boundary_host_set_plugin" "foobar" {
host_catalog_id = boundary_host_catalog_plugin.aws_example.id
preferred_endpoints = ["cidr:54.0.0.0/8"]
attributes_json = jsonencode({
"filters" = "tag-key=foo",
"filters" = "tag-key=bar"
"filters" = ["tag-key=foo", "tag-key=bar"]
})
}
@@ -54,8 +53,7 @@ resource "boundary_host_set_plugin" "launch" {
host_catalog_id = boundary_host_catalog_plugin.aws_example.id
sync_interval_seconds = 60
attributes_json = jsonencode({
"filters" = "tag:development=prod,dev",
"filters" = "launch-time=2022-01-04T*"
"filters" = ["tag:development=prod,dev", "launch-time=2022-01-04T*"]
})
}
@@ -0,0 +1 @@
terraform import boundary_storage_bucket.foo <my-id>
@@ -0,0 +1,24 @@
resource "boundary_scope" "org" {
name = "organization_one"
description = "My first scope!"
scope_id = boundary_scope.global.id
auto_create_admin_role = true
auto_create_default_role = true
}
resource "boundary_storage_bucket" "aws_example" {
name = "My aws storage bucket"
description = "My first storage bucket!"
scope_id = boundary_scope.org.id
plugin_name = "aws"
bucket_name = "mybucket"
attributes_json = jsonencode({ "region" = "us-east-1" })
# recommended to pass in aws secrets using a file() or using environment variables
# the secrets below must be generated in aws by creating a aws iam user with programmatic access
secrets_json = jsonencode({
"access_key_id" = "aws_access_key_id_value",
"secret_access_key" = "aws_secret_access_key_value"
})
worker_filter = "\"pki\" in \"/tags/type\""
}
@@ -69,6 +69,20 @@ resource "boundary_host_set" "foo" {
]
}
resource "boundary_storage_bucket" "aws_example" {
name = "My aws storage bucket"
description = "My first storage bucket!"
scope_id = boundary_scope.org.id
plugin_name = "aws"
bucket_name = "mybucket"
attributes_json = jsonencode({ "region" = "us-east-1" })
secrets_json = jsonencode({
"access_key_id" = "aws_access_key_id_value",
"secret_access_key" = "aws_secret_access_key_value"
})
worker_filter = "\"pki\" in \"/tags/type\""
}
resource "boundary_target" "foo" {
name = "foo"
description = "Foo target"
@@ -97,6 +111,22 @@ resource "boundary_target" "ssh_foo" {
]
}
resource "boundary_target" "ssh_session_recording_foo" {
name = "ssh_foo"
description = "Ssh target"
type = "ssh"
default_port = "22"
scope_id = boundary_scope.project.id
host_source_ids = [
boundary_host_set.foo.id
]
injected_application_credential_source_ids = [
boundary_credential_library_vault.foo.id
]
enable_session_recording = true
storage_bucket_id = boundary_storage_bucket.aws_example
}
resource "boundary_target" "address_foo" {
name = "address_foo"
description = "Foo target with an address"
+2
View File
@@ -44,4 +44,6 @@ const (
// internalForceUpdateKey is used to force updates so we can always check
// the value of secrets
internalForceUpdateKey = "internal_force_update"
// workerFilter is used for common "worker_filter" resource attribute
WorkerFilterKey = "worker_filter"
)
+1
View File
@@ -122,6 +122,7 @@ func New() *schema.Provider {
"boundary_host_set_plugin": resourceHostSetPlugin(),
"boundary_role": resourceRole(),
"boundary_scope": resourceScope(),
"boundary_storage_bucket": resourceStorageBucket(),
"boundary_target": resourceTarget(),
"boundary_user": resourceUser(),
"boundary_worker": resourceWorker(),
@@ -0,0 +1,552 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"net/http"
"strconv"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/storagebuckets"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
storageBucketNameKey = "bucket_name"
storageBucketPrefixKey = "bucket_prefix"
)
func resourceStorageBucket() *schema.Resource {
return &schema.Resource{
Description: `The storage bucket resource allows you to configure a Boundary storage bucket. ` +
`A storage bucket can only belong to the Global scope or an Org scope. ` +
`At this time, the only supported storage for storage buckets is AWS S3. ` +
`This feature requires Boundary Enterprise or Boundary HCP.`,
CreateContext: resourceStorageBucketCreate,
ReadContext: resourceStorageBucketRead,
UpdateContext: resourceStorageBucketUpdate,
DeleteContext: resourceStorageBucketDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
IDKey: {
Description: "The ID of the storage bucket.",
Type: schema.TypeString,
Computed: true,
},
NameKey: {
Description: "The storage bucket name. Defaults to the resource name.",
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Description: "The storage bucket description.",
Type: schema.TypeString,
Optional: true,
},
PluginIdKey: {
Description: "The ID of the plugin that should back the resource. This or " + PluginNameKey + " must be defined.",
Type: schema.TypeString,
ConflictsWith: []string{PluginNameKey},
ExactlyOneOf: []string{PluginIdKey, PluginNameKey},
Optional: true,
ForceNew: true,
Computed: true, // If name is provided this will be computed
},
PluginNameKey: {
Description: "The name of the plugin that should back the resource. This or " + PluginIdKey + " must be defined.",
Type: schema.TypeString,
ConflictsWith: []string{PluginIdKey},
ExactlyOneOf: []string{PluginIdKey, PluginNameKey},
Optional: true,
ForceNew: true,
},
ScopeIdKey: {
Description: "The scope for this storage bucket.",
Type: schema.TypeString,
ForceNew: true,
Required: true,
},
SecretsJsonKey: {
Description: `The secrets for the storage bucket. Either values encoded with the "jsonencode" function, pre-escaped JSON string, ` +
`or a file:// or env:// path. Set to a string "null" to clear any existing values. NOTE: Unlike "attributes_json", removing ` +
`this block will NOT clear secrets from the storage bucket; this allows injecting secrets for one call, then removing them for storage.`,
Type: schema.TypeString,
Required: true,
Sensitive: true,
},
SecretsHmacKey: {
Description: "The HMAC'd secrets value returned from the server.",
Type: schema.TypeString,
Computed: true,
},
storageBucketNameKey: {
Description: "The name of the bucket within the external object store service.",
Type: schema.TypeString,
Required: true,
},
storageBucketPrefixKey: {
Description: "The prefix used to organize the data held within the external object store.",
Type: schema.TypeString,
Optional: true,
},
internalSecretsConfigHmacKey: {
Description: "Internal only. HMAC of (serverSecretsHmac + config secrets). Used for proper secrets handling.",
Type: schema.TypeString,
Computed: true,
},
internalHmacUsedForSecretsConfigHmacKey: {
Description: "Internal only. The Boundary-provided HMAC used to calculate the current value of the HMAC'd config. Used for drift detection.",
Type: schema.TypeString,
Computed: true,
},
AttributesJsonKey: {
Description: `The attributes for the storage bucket. The "region" attribute field is required when creating an AWS storage bucket. ` +
`Values are either encoded with the "jsonencode" function, pre-escaped JSON string, ` +
`or a file:// or env:// path. Set to a string "null" or remove the block to clear all attributes in the storage bucket.`,
Type: schema.TypeString,
Optional: true,
// If set to null in config and nothing comes from API, consider
// it the same. Same if config changes from empty to null.
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
sanitizedNew, err := sanitizeJson(new)
if err != nil {
return false
}
new = string(sanitizedNew)
switch {
case old == new:
return true
case old == "null" && new == "":
return true
case old == "" && new == "null":
return true
default:
return false
}
},
},
WorkerFilterKey: {
Description: `Filters to the worker(s) that can handle requests for this storage bucket. The filter must match an existing ` +
`worker in order to create a storage bucket.`,
Type: schema.TypeString,
Required: true,
},
internalForceUpdateKey: {
Description: "Internal only. Used to force update so that we can always check the value of secrets.",
Type: schema.TypeString,
Computed: true,
},
},
// We want to always force an update (which itself may not actually do
// anything) so that we can properly check secrets state.
CustomizeDiff: func(_ context.Context, d *schema.ResourceDiff, _ interface{}) error {
return d.SetNewComputed(internalForceUpdateKey)
},
}
}
func setFromStorageBucketResponseMap(d *schema.ResourceData, raw map[string]interface{}) error {
if err := d.Set(NameKey, raw[NameKey]); err != nil {
return err
}
if err := d.Set(DescriptionKey, raw[DescriptionKey]); err != nil {
return err
}
if err := d.Set(ScopeIdKey, raw[ScopeIdKey]); err != nil {
return err
}
// Plugin stuff
{
if err := d.Set(PluginIdKey, raw[PluginIdKey]); err != nil {
return err
}
pluginRaw, ok := raw["plugin"]
if !ok {
return fmt.Errorf("plugin field not found in response")
}
pluginInfo, ok := pluginRaw.(map[string]interface{})
if !ok {
return fmt.Errorf("plugin field in response has wrong type")
}
pluginNameRaw, ok := pluginInfo["name"]
if !ok {
return fmt.Errorf("plugin name field not found in response")
}
pluginName, ok := pluginNameRaw.(string)
if !ok {
return fmt.Errorf("plugin name field in response has wrong type")
}
if err := d.Set(PluginNameKey, pluginName); err != nil {
return err
}
}
// Attributes stuff
{
attrRaw, ok := raw["attributes"]
switch ok {
case true:
encodedAttributes, err := json.Marshal(attrRaw)
if err != nil {
return err
}
if err := d.Set(AttributesJsonKey, string(encodedAttributes)); err != nil {
return err
}
default:
d.Set(AttributesJsonKey, nil)
}
}
// Secrets stuff
{
// We do not save secrets into the state file, and they're not returned in
// the response
secretsHmacRaw, ok := raw[SecretsHmacKey]
switch ok {
case true:
if err := d.Set(SecretsHmacKey, secretsHmacRaw); err != nil {
return err
}
default:
d.Set(SecretsHmacKey, nil)
}
}
d.SetId(raw[IDKey].(string))
if err := d.Set(internalForceUpdateKey, strconv.FormatInt(rand.Int63(), 10)); err != nil {
return err
}
return nil
}
func resourceStorageBucketCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
sbClient := storagebuckets.NewClient(md.client)
opts := []storagebuckets.Option{}
var scopeId string
if scopeIdVal, ok := d.GetOk(ScopeIdKey); ok {
scopeId = scopeIdVal.(string)
} else {
return diag.Errorf("no scope ID provided")
}
if pluginIdVal, ok := d.GetOk(PluginIdKey); ok {
opts = append(opts, storagebuckets.WithPluginId(pluginIdVal.(string)))
}
if pluginNameVal, ok := d.GetOk(PluginNameKey); ok {
opts = append(opts, storagebuckets.WithPluginName(pluginNameVal.(string)))
}
if nameVal, ok := d.GetOk(NameKey); ok {
opts = append(opts, storagebuckets.WithName(nameVal.(string)))
}
if descVal, ok := d.GetOk(DescriptionKey); ok {
opts = append(opts, storagebuckets.WithDescription(descVal.(string)))
}
if storageBucketNameVal, ok := d.GetOk(storageBucketNameKey); ok {
opts = append(opts, storagebuckets.WithBucketName(storageBucketNameVal.(string)))
}
if storageBucketPrefixVal, ok := d.GetOk(storageBucketPrefixKey); ok {
opts = append(opts, storagebuckets.WithBucketPrefix(storageBucketPrefixVal.(string)))
}
if workerFilterVal, ok := d.GetOk(WorkerFilterKey); ok {
opts = append(opts, storagebuckets.WithWorkerFilter(workerFilterVal.(string)))
}
attrsVal, ok := d.GetOk(AttributesJsonKey)
if ok {
attrsStr, err := parseutil.ParsePath(attrsVal.(string))
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return diag.Errorf("error parsing path with attributes: %v", err)
}
switch attrsStr {
case "null":
opts = append(opts, storagebuckets.DefaultAttributes())
default:
// What comes in is json-encoded but we want to set a
// map[string]interface{} so we unmarshal it and set that
var m map[string]interface{}
if err := json.Unmarshal([]byte(attrsStr), &m); err != nil {
return diag.Errorf("error unmarshaling attributes: %v", err)
}
opts = append(opts, storagebuckets.WithAttributes(m))
}
}
secretsVal, ok := d.GetOk(SecretsJsonKey)
var secretsJson string
if ok {
var err error
secretsJson, err = parseutil.ParsePath(secretsVal.(string))
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return diag.Errorf("error parsing path with secrets: %v", err)
}
switch secretsJson {
case "null":
opts = append(opts, storagebuckets.DefaultSecrets())
default:
// What comes in is json-encoded but we want to set a
// map[string]interface{} so we unmarshal it and set that
var m map[string]interface{}
if err := json.Unmarshal([]byte(secretsJson), &m); err != nil {
return diag.Errorf("error unmarshaling secrets: %v", err)
}
opts = append(opts, storagebuckets.WithSecrets(m))
}
}
sbr, err := sbClient.Create(ctx, scopeId, opts...)
if err != nil {
return diag.Errorf("error creating storage bucket: %v", err)
}
if sbr == nil {
return diag.Errorf("nil storage bucket after create")
}
if err := setFromStorageBucketResponseMap(d, sbr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
if serverHmac := d.Get(SecretsHmacKey).(string); serverHmac != "" {
configHmac, err := calculateCurrentConfigHmac(serverHmac, secretsJson)
if err != nil {
return diag.FromErr(err)
}
if err := d.Set(internalSecretsConfigHmacKey, configHmac); err != nil {
return diag.FromErr(err)
}
if err := d.Set(internalHmacUsedForSecretsConfigHmacKey, serverHmac); err != nil {
return diag.FromErr(err)
}
}
return nil
}
func resourceStorageBucketRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
sbClient := storagebuckets.NewClient(md.client)
sbrr, err := sbClient.Read(ctx, d.Id())
if err != nil {
if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound {
d.SetId("")
return nil
}
return diag.Errorf("error reading storage bucket: %v", err)
}
if sbrr == nil {
return diag.Errorf("storage bucket nil after read")
}
if err := setFromStorageBucketResponseMap(d, sbrr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
return nil
}
func resourceStorageBucketUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
sbClient := storagebuckets.NewClient(md.client)
opts := []storagebuckets.Option{}
// We need to refresh the current server hmac value to figure out what to do
// next around secrets handling
var clearStateSecrets, sendSecretsToBoundary bool
var secretsJson string
var currentDiagnostics diag.Diagnostics
{
sbrr, err := sbClient.Read(ctx, d.Id())
if err != nil {
if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound {
d.SetId("")
return nil
}
return diag.Errorf("error reading storage bucket in update: %v", err)
}
if sbrr == nil {
return diag.Errorf("storage bucket nil after read in update")
}
var serverSecretsHmac string
if secretsHmacRaw, ok := sbrr.GetResponse().Map[SecretsHmacKey]; ok {
serverSecretsHmac = secretsHmacRaw.(string)
}
// Get current secrets_json value
secretsJson, err = parseutil.ParsePath(d.Get(SecretsJsonKey).(string))
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return diag.Errorf("error parsing path with secrets: %v", err)
}
// Now that we have the value from the server, see if anything needs to be
// done
var diagWarning *diag.Diagnostic
clearStateSecrets, sendSecretsToBoundary, diagWarning, err = calculateConfigHmacPlan(serverSecretsHmac, secretsJson, d)
if err != nil {
return diag.FromErr(err)
}
if diagWarning != nil {
currentDiagnostics = append(currentDiagnostics, *diagWarning)
}
}
if d.HasChange(NameKey) {
opts = append(opts, storagebuckets.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, storagebuckets.WithName(nameStr))
}
}
if d.HasChange(DescriptionKey) {
opts = append(opts, storagebuckets.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, storagebuckets.WithDescription(descStr))
}
}
if d.HasChange(storageBucketNameKey) {
opts = append(opts, storagebuckets.DefaultBucketName())
storageBucketNameVal, ok := d.GetOk(storageBucketNameKey)
if ok {
storageBucketNameStr := storageBucketNameVal.(string)
opts = append(opts, storagebuckets.WithBucketName(storageBucketNameStr))
}
}
if d.HasChange(storageBucketPrefixKey) {
opts = append(opts, storagebuckets.DefaultBucketPrefix())
storageBucketPrefixVal, ok := d.GetOk(storageBucketPrefixKey)
if ok {
storageBucketPrefixStr := storageBucketPrefixVal.(string)
opts = append(opts, storagebuckets.WithBucketPrefix(storageBucketPrefixStr))
}
}
if d.HasChange(WorkerFilterKey) {
opts = append(opts, storagebuckets.DefaultWorkerFilter())
workerFilterVal, ok := d.GetOk(WorkerFilterKey)
if ok {
workerFilterStr := workerFilterVal.(string)
opts = append(opts, storagebuckets.WithWorkerFilter(workerFilterStr))
}
}
if d.HasChange(AttributesJsonKey) {
attrsVal, ok := d.GetOk(AttributesJsonKey)
if ok {
attrsStr, err := parseutil.ParsePath(attrsVal.(string))
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return append(currentDiagnostics, diag.Errorf("error parsing path with attributes: %v", err)...)
}
switch attrsStr {
case "null", "":
opts = append(opts, storagebuckets.DefaultAttributes())
default:
// What comes in is json-encoded but we want to set a
// map[string]interface{} so we unmarshal it and set that
var m map[string]interface{}
if err := json.Unmarshal([]byte(attrsStr), &m); err != nil {
return append(currentDiagnostics, diag.Errorf("error unmarshaling attributes: %v", err)...)
}
opts = append(opts, storagebuckets.WithAttributes(m))
}
} else {
opts = append(opts, storagebuckets.DefaultAttributes())
}
}
if sendSecretsToBoundary {
switch secretsJson {
case "null":
opts = append(opts, storagebuckets.DefaultSecrets())
default:
// What comes in is json-encoded but we want to set a
// map[string]interface{} so we unmarshal it and set that
var m map[string]interface{}
if err := json.Unmarshal([]byte(secretsJson), &m); err != nil {
return append(currentDiagnostics, diag.Errorf("error unmarshaling secrets: %v", err)...)
}
opts = append(opts, storagebuckets.WithSecrets(m))
}
}
if len(opts) > 0 {
opts = append(opts, storagebuckets.WithAutomaticVersioning(true))
sbur, err := sbClient.Update(ctx, d.Id(), 0, opts...)
if err != nil {
return append(currentDiagnostics, diag.Errorf("error updating storage bucket: %v", err)...)
}
if sbur == nil {
return append(currentDiagnostics, diag.Errorf("storage bucket nil after update")...)
}
if err := setFromStorageBucketResponseMap(d, sbur.GetResponse().Map); err != nil {
return append(currentDiagnostics, diag.FromErr(err)...)
}
}
// Save any updated secrets information if needed
switch {
case clearStateSecrets:
if err := d.Set(internalSecretsConfigHmacKey, nil); err != nil {
return append(currentDiagnostics, diag.FromErr(err)...)
}
if err := d.Set(internalHmacUsedForSecretsConfigHmacKey, nil); err != nil {
return append(currentDiagnostics, diag.FromErr(err)...)
}
case sendSecretsToBoundary:
if serverHmac := d.Get(SecretsHmacKey).(string); serverHmac != "" {
configHmac, err := calculateCurrentConfigHmac(serverHmac, secretsJson)
if err != nil {
return append(currentDiagnostics, diag.FromErr(err)...)
}
if err := d.Set(internalSecretsConfigHmacKey, configHmac); err != nil {
return append(currentDiagnostics, diag.FromErr(err)...)
}
if err := d.Set(internalHmacUsedForSecretsConfigHmacKey, serverHmac); err != nil {
return append(currentDiagnostics, diag.FromErr(err)...)
}
}
}
return nil
}
func resourceStorageBucketDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
sbClient := storagebuckets.NewClient(md.client)
_, err := sbClient.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error deleting storage bucket: %v", err)
}
return nil
}
@@ -0,0 +1,369 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"testing"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/storagebuckets"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
const (
testStorageBucketDescription = "bar"
testStorageBucketDescriptionUpdate = "bar foo"
testStorageBucketDescriptionUpdate2 = "bar foo foo"
)
var projStorageBucketBase = `
resource "boundary_storage_bucket" "foo" {
name = "foo"
scope_id = boundary_scope.proj1.id
plugin_name = "loopback"
bucket_name = "testbucket123"
worker_filter = "\"pki\" in \"/tags/type\""
%s
depends_on = [boundary_role.proj1_admin]
}`
var (
currentStorageBucketSecretsHmacValue string
currentStorageBucketAttributesValue string
)
func TestAccStorageBucket(t *testing.T) {
t.Skip("Skipping test until Boundary Terraform Provider can unit tests for Boundary Enterprise only features")
tc := controller.NewTestController(t, tcConfig...)
defer tc.Shutdown()
url := tc.ApiAddrs()[0]
resName := "boundary_storage_bucket.foo"
workerFilter := "\"pki\" in \"/tags/type\""
initialValuesStr := fmt.Sprintf(`
description = "%s"
attributes_json = jsonencode({
foo = "bar"
zip = "zap"
})
workerFilter = "%s"
secrets_json = jsonencode({
hush = "puppies"
})
`,
testStorageBucketDescription,
workerFilter,
)
// Changed description and secrets
valuesStrUpdate1 := fmt.Sprintf(`
description = "%s"
attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})
workerFilter = "%s"
secrets_json = jsonencode({
flush = "fluppies"
})
`,
testStorageBucketDescriptionUpdate,
workerFilter,
)
// Changed description, no secrets update
valuesStrUpdate2 := fmt.Sprintf(`
description = "%s"
attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})
workerFilter = "%s"
secrets_json = jsonencode({
flush = "fluppies"
})
`,
testStorageBucketDescriptionUpdate2,
workerFilter,
)
// Same description, now explicitly unset secrets and blankify attrs
valuesStrUpdate3 := fmt.Sprintf(`
description = "%s"
secrets_json = jsonencode({
flush = "fluppies"
})
`,
testStorageBucketDescriptionUpdate2,
)
// Set values again
valuesStrUpdate4 := fmt.Sprintf(`
description = "%s"
attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})
secrets_json = jsonencode({
flush = "fluppies"
})
`,
testStorageBucketDescriptionUpdate2,
)
// Explicitly set both secrets and attributes to null
valuesStrUpdate5 := fmt.Sprintf(`
description = "%s"
attributes_json = "null"
secrets_json = "null"
`,
testStorageBucketDescriptionUpdate2,
)
initialHcl := fmt.Sprintf(projStorageBucketBase, initialValuesStr)
update1Hcl := fmt.Sprintf(projStorageBucketBase, valuesStrUpdate1)
update2Hcl := fmt.Sprintf(projStorageBucketBase, valuesStrUpdate2)
update3Hcl := fmt.Sprintf(projStorageBucketBase, valuesStrUpdate3)
update4Hcl := fmt.Sprintf(projStorageBucketBase, valuesStrUpdate4)
update5Hcl := fmt.Sprintf(projStorageBucketBase, valuesStrUpdate5)
var provider *schema.Provider
resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories(&provider),
CheckDestroy: testAccCheckStorageBucketResourceDestroy(t, provider, "boundary_storage_bucket"),
Steps: []resource.TestStep{
{
// test create
Config: testConfig(url, fooOrg, firstProjectFoo, initialHcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckScopeResourceExists(provider, "boundary_scope.org1"),
testAccCheckScopeResourceExists(provider, "boundary_scope.proj1"),
testAccCheckStorageBucketResourceExists(provider, resName, expectedAttributesStatePreviouslyEmptyNowSet),
resource.TestCheckResourceAttr(resName, DescriptionKey, testStorageBucketDescription),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update1Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckStorageBucketResourceExists(provider, resName, expectedAttributesStatePreviouslySetButChanged),
resource.TestCheckResourceAttr(resName, DescriptionKey, testStorageBucketDescriptionUpdate),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update2Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckStorageBucketResourceExists(provider, resName, expectedAttributesStatePreviouslySetNoChange),
resource.TestCheckResourceAttr(resName, DescriptionKey, testStorageBucketDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// this runs the same HCL; mostly used in some manual checking
// to ensure update is still called even when nothing has
// changed (as we need for secrets)
Config: testConfig(url, fooOrg, firstProjectFoo, update2Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckStorageBucketResourceExists(provider, resName, expectedAttributesStatePreviouslySetNoChange),
resource.TestCheckResourceAttr(resName, DescriptionKey, testStorageBucketDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update3Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckStorageBucketResourceExists(provider, resName, expectedAttributesStatePreviouslySetNoChange),
resource.TestCheckResourceAttr(resName, DescriptionKey, testStorageBucketDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update4Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckStorageBucketResourceExists(provider, resName, expectedAttributesStatePreviouslySetNowEmpty),
resource.TestCheckResourceAttr(resName, DescriptionKey, testStorageBucketDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update5Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckStorageBucketResourceExists(provider, resName, expectedAttributesStatePreviouslyEmptyNowSet),
resource.TestCheckResourceAttr(resName, DescriptionKey, testStorageBucketDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
},
})
}
func testAccCheckStorageBucketResourceExists(testProvider *schema.Provider, name string, expAttrState expectedAttributesState) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}
id := rs.Primary.ID
if id == "" {
return fmt.Errorf("No ID is set")
}
md := testProvider.Meta().(*metaData)
sbClient := storagebuckets.NewClient(md.client)
val, err := sbClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading storage bucket %q: %v", id, err)
}
if val == nil {
return errors.New("empty val returned")
}
secretsHmac := val.GetResponse().Map[SecretsHmacKey]
attrs := val.GetResponse().Map["attributes"]
switch expAttrState {
case expectedAttributesStatePreviouslyEmptyNowSet:
if currentStorageBucketSecretsHmacValue != "" {
return errors.New("expected no previous secrets hmac value")
}
val := secretsHmac.(string)
if val == "" {
return errors.New("expected non-empty new secrets hmac value")
}
currentStorageBucketSecretsHmacValue = val
if currentStorageBucketAttributesValue != "" {
return fmt.Errorf("expected no previous attributes value, got %s", currentStorageBucketAttributesValue)
}
if attrs == nil {
return errors.New("expected non-empty new attributes value")
}
attrsVal, err := json.Marshal(attrs)
if err != nil {
return fmt.Errorf("error marshaling attrs: %w", err)
}
currentStorageBucketAttributesValue = string(attrsVal)
case expectedAttributesStatePreviouslySetButChanged:
if currentStorageBucketSecretsHmacValue == "" {
return errors.New("expected previous secrets hmac value")
}
val := secretsHmac.(string)
if val == "" {
return errors.New("expected non-empty new secrets hmac value")
}
if val == currentStorageBucketSecretsHmacValue {
return errors.New("expected changed secrets hmac value")
}
currentStorageBucketSecretsHmacValue = val
if currentStorageBucketAttributesValue == "" {
return errors.New("expected previous attributes value")
}
if attrs == nil {
return errors.New("expected non-empty new attributes value")
}
attrsVal, err := json.Marshal(attrs)
if err != nil {
return fmt.Errorf("error marshaling attrs: %w", err)
}
if string(attrsVal) == currentStorageBucketAttributesValue {
return errors.New("expected changed attrs value")
}
currentStorageBucketAttributesValue = string(attrsVal)
case expectedAttributesStatePreviouslySetNoChange:
if currentStorageBucketSecretsHmacValue == "" {
return errors.New("expected previous secrets hmac value")
}
val := secretsHmac.(string)
if val == "" {
return errors.New("expected non-empty new secrets hmac value")
}
if val != currentStorageBucketSecretsHmacValue {
return errors.New("expected same secrets hmac value")
}
if currentStorageBucketAttributesValue == "" {
return errors.New("expected previous attributes value")
}
if attrs == nil {
return errors.New("expected non-empty new attributes value")
}
attrsVal, err := json.Marshal(attrs)
if err != nil {
return fmt.Errorf("error marshaling attrs: %w", err)
}
if string(attrsVal) == "" {
return errors.New("expected non-empty new attributes value")
}
if string(attrsVal) != currentStorageBucketAttributesValue {
return errors.New("expected same attributes value")
}
case expectedAttributesStatePreviouslySetNowEmpty:
if currentStorageBucketSecretsHmacValue == "" {
return errors.New("expected previous secrets hmac value")
}
if secretsHmac != nil {
return fmt.Errorf("expected empty new secrets hmac value, got %s", secretsHmac.(string))
}
currentStorageBucketSecretsHmacValue = ""
if currentStorageBucketAttributesValue == "" {
return errors.New("expected previous attributes value")
}
if attrs != nil {
return fmt.Errorf("expected empty new attributes value, got %s", attrs)
}
currentStorageBucketAttributesValue = ""
}
return nil
}
}
func testAccCheckStorageBucketResourceDestroy(t *testing.T, testProvider *schema.Provider, typ string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if testProvider.Meta() == nil {
t.Fatal("got nil provider metadata")
}
md := testProvider.Meta().(*metaData)
for _, rs := range s.RootModule().Resources {
switch rs.Type {
case string(typ):
id := rs.Primary.ID
sbClient := storagebuckets.NewClient(md.client)
_, err := sbClient.Read(context.Background(), id)
if apiErr := api.AsServerError(err); apiErr == nil || apiErr.Response().StatusCode() != http.StatusNotFound {
return fmt.Errorf("didn't get a 404 when reading destroyed storage bucket %q: %v", id, err)
}
default:
continue
}
}
return nil
}
}
+79 -1
View File
@@ -21,8 +21,10 @@ const (
targetInjectedAppCredentialSourceIdsKey = "injected_application_credential_source_ids"
targetDefaultPortKey = "default_port"
targetDefaultClientPortKey = "default_client_port"
targetEnableSessionRecordingKey = "enable_session_recording"
targetSessionMaxSecondsKey = "session_max_seconds"
targetSessionConnectionLimitKey = "session_connection_limit"
targetStorageBucketIdKey = "storage_bucket_id"
targetWorkerFilterKey = "worker_filter"
targetWorkerEgressFilterKey = "egress_worker_filter"
targetWorkerIngressFilterKey = "ingress_worker_filter"
@@ -133,6 +135,16 @@ func resourceTarget() *schema.Resource {
Optional: true,
ConflictsWith: []string{targetHostSourceIdsKey},
},
targetEnableSessionRecordingKey: {
Description: "HCP/Ent Only. Enable sessions recording for this target. Only applicable for SSH targets.",
Type: schema.TypeBool,
Optional: true,
},
targetStorageBucketIdKey: {
Description: "HCP/Ent Only. Storage bucket for this target. Only applicable for SSH targets.",
Type: schema.TypeString,
Optional: true,
},
},
}
}
@@ -178,7 +190,9 @@ func setFromTargetResponseMap(d *schema.ResourceData, raw map[string]interface{}
return err
}
switch raw["type"].(string) {
typeStr := raw["type"].(string)
switch typeStr {
case targetTypeTcp, targetTypeSsh:
if attrsVal, ok := raw["attributes"]; ok {
attrs := attrsVal.(map[string]interface{})
@@ -194,6 +208,20 @@ func setFromTargetResponseMap(d *schema.ResourceData, raw map[string]interface{}
return err
}
}
// SSH only features
if typeStr == targetTypeSsh {
if sessionRecordingVal, ok := attrs[targetEnableSessionRecordingKey].(bool); ok {
if err := d.Set(targetEnableSessionRecordingKey, sessionRecordingVal); err != nil {
return err
}
}
if targetStorageBucketIdVal, ok := attrs[targetStorageBucketIdKey]; ok {
if err := d.Set(targetStorageBucketIdKey, targetStorageBucketIdVal); err != nil {
return err
}
}
}
}
}
@@ -264,6 +292,18 @@ func resourceTargetCreate(ctx context.Context, d *schema.ResourceData, meta inte
}
}
enableSessionRecordingVal, ok := d.GetOk(targetEnableSessionRecordingKey)
if ok {
enableSessionRecordingBool := enableSessionRecordingVal.(bool)
opts = append(opts, targets.WithSshTargetEnableSessionRecording(enableSessionRecordingBool))
}
storageBucketIdVal, ok := d.GetOk(targetStorageBucketIdKey)
if ok {
storageBucketIdStr := storageBucketIdVal.(string)
opts = append(opts, targets.WithSshTargetStorageBucketId(storageBucketIdStr))
}
sessionMaxSecondsVal, ok := d.GetOk(targetSessionMaxSecondsKey)
if ok {
sessionMaxSecondsInt := sessionMaxSecondsVal.(int)
@@ -443,6 +483,34 @@ func resourceTargetUpdate(ctx context.Context, d *schema.ResourceData, meta inte
}
}
var enableSessionRecording *bool
if d.HasChange(targetEnableSessionRecordingKey) {
switch typeStr {
case targetTypeSsh:
opts = append(opts, targets.WithSshTargetEnableSessionRecording(false))
enableSessionRecordingVal, ok := d.GetOk(targetEnableSessionRecordingKey)
if ok {
enableSessionRecordingBool := enableSessionRecordingVal.(bool)
enableSessionRecording = &enableSessionRecordingBool
opts = append(opts, targets.WithSshTargetEnableSessionRecording(enableSessionRecordingBool))
}
}
}
var storageBucket *string
if d.HasChange(targetStorageBucketIdKey) {
switch typeStr {
case targetTypeSsh:
opts = append(opts, targets.DefaultSshTargetStorageBucketId())
storageBucketVal, ok := d.GetOk(targetStorageBucketIdKey)
if ok {
storageBucketStr := storageBucketVal.(string)
storageBucket = &storageBucketStr
opts = append(opts, targets.WithSshTargetStorageBucketId(storageBucketStr))
}
}
}
var defaultPort *int
if d.HasChange(targetDefaultPortKey) {
switch typeStr {
@@ -631,6 +699,16 @@ func resourceTargetUpdate(ctx context.Context, d *schema.ResourceData, meta inte
return diag.FromErr(err)
}
}
if d.HasChange(targetEnableSessionRecordingKey) {
if err := d.Set(targetEnableSessionRecordingKey, enableSessionRecording); err != nil {
return diag.FromErr(err)
}
}
if d.HasChange(targetStorageBucketIdKey) {
if err := d.Set(targetStorageBucketIdKey, storageBucket); err != nil {
return diag.FromErr(err)
}
}
// The above call may not actually happen, so we use d.Id() and automatic
// versioning here