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
This commit is contained in:
1 parent
1c05eab410
commit
9186304d5d
8 files changed
+1029
No files matched your search
@@ -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"
|
||||
)
|
||||
@@ -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.DefaultBucketName())
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user