Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for retrying identity entity reads #1263

Merged
merged 8 commits into from
Dec 20, 2021
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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-hclog v1.0.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-retryablehttp v0.7.0
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.5
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2
github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.0
Expand Down
54 changes: 54 additions & 0 deletions util/util.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package util

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"reflect"
"regexp"
"strings"
"testing"
"time"

"github.com/hashicorp/go-retryablehttp"
"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"
"github.com/hashicorp/vault/api"
)

func JsonDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
Expand Down Expand Up @@ -328,3 +332,53 @@ func PathParameters(endpoint, vaultPath string) (map[string]string, error) {
}
return result, nil
}

// StatusCheckRetry for any response having a status code in statusCode.
func StatusCheckRetry(statusCodes ...int) retryablehttp.CheckRetry {
return func(ctx context.Context, resp *http.Response, err error) (bool, error) {
// ensure that the client controlled consistency policy is honoured.
if retry, err := api.DefaultRetryPolicy(ctx, resp, err); err != nil || retry {
return retry, err
}

if resp != nil {
for _, code := range statusCodes {
if code == resp.StatusCode {
return true, nil
}
}
}
return false, nil
}
}

// SetupCCCRetryClient for handling Client Controlled Consistency related
// requests.
func SetupCCCRetryClient(client *api.Client, maxRetry int) {
if !client.ReadYourWrites() {
client.SetReadYourWrites(true)
}

client.SetMaxRetries(maxRetry)
client.SetCheckRetry(StatusCheckRetry(http.StatusNotFound))

// ensure that the clone has the reasonable backoff min/max durations set.
if client.MinRetryWait() == 0 {
client.SetMinRetryWait(time.Millisecond * 1000)
}
if client.MaxRetryWait() == 0 {
client.SetMaxRetryWait(time.Millisecond * 1500)
}
if client.MaxRetryWait() < client.MinRetryWait() {
client.SetMaxRetryWait(client.MinRetryWait())
}

bo := retryablehttp.LinearJitterBackoff
client.SetBackoff(bo)

to := time.Duration(0)
for i := 0; i < client.MaxRetries(); i++ {
to += bo(client.MaxRetryWait(), client.MaxRetryWait(), i, nil)
}
client.SetClientTimeout(to + time.Second*30)
}
28 changes: 23 additions & 5 deletions vault/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,17 @@ const (
// versions of Vault.
// We aim to deprecate items in this category.
UnknownPath = "unknown"

// DefaultMaxHTTPRetries is used for configuring the api.Client's MaxRetries.
DefaultMaxHTTPRetries = 2

// DefaultMaxHTTPRetriesCCC is used for configuring the api.Client's MaxRetries
// for Client Controlled Consistency related operations.
DefaultMaxHTTPRetriesCCC = 10
)

var maxHTTPRetriesCCC int

// This is a global MutexKV for use within this provider.
// Use this when you need to have multiple resources or even multiple instances
// of the same resource write to the same path in Vault.
Expand Down Expand Up @@ -157,16 +166,20 @@ func Provider() *schema.Provider {
// significantly longer, so that any leases are revoked shortly
// after Terraform has finished running.
DefaultFunc: schema.EnvDefaultFunc("TERRAFORM_VAULT_MAX_TTL", 1200),

Description: "Maximum TTL for secret leases requested by this provider.",
},
"max_retries": {
Type: schema.TypeInt,
Optional: true,

DefaultFunc: schema.EnvDefaultFunc("VAULT_MAX_RETRIES", 2),
Type: schema.TypeInt,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("VAULT_MAX_RETRIES", DefaultMaxHTTPRetries),
Description: "Maximum number of retries when a 5xx error code is encountered.",
},
"max_retries_ccc": {
Type: schema.TypeInt,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("VAULT_MAX_RETRIES_CCC", DefaultMaxHTTPRetriesCCC),
Description: "Maximum number of retries for Client Controlled Consistency related operations",
},
"namespace": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -757,6 +770,9 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
// enable ReadYourWrites to support read-after-write on Vault Enterprise
clientConfig.ReadYourWrites = true

// set default MaxRetries
clientConfig.MaxRetries = DefaultMaxHTTPRetries

client, err := api.NewClient(clientConfig)
if err != nil {
return nil, fmt.Errorf("failed to configure Vault API: %s", err)
Expand All @@ -782,6 +798,8 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {

client.SetMaxRetries(d.Get("max_retries").(int))

maxHTTPRetriesCCC = d.Get("max_retries_ccc").(int)

// Try an get the token from the config or token helper
token, err := providerToken(d)
if err != nil {
Expand Down
49 changes: 36 additions & 13 deletions vault/resource_identity_entity.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package vault

import (
"errors"
"fmt"
"log"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-vault/util"
"github.com/hashicorp/vault/api"

"github.com/hashicorp/terraform-provider-vault/util"
)

const identityEntityPath = "/identity/entity"

var errEntityNotFound = errors.New("entity not found")

func identityEntityResource() *schema.Resource {
return &schema.Resource{
Create: identityEntityCreate,
Expand Down Expand Up @@ -117,7 +121,6 @@ func identityEntityCreate(d *schema.ResourceData, meta interface{}) error {
identityEntityUpdateFields(d, data, true)

resp, err := client.Logical().Write(path, data)

if err != nil {
return fmt.Errorf("error writing IdentityEntity to %q: %s", name, err)
}
Expand Down Expand Up @@ -155,7 +158,6 @@ func identityEntityUpdate(d *schema.ResourceData, meta interface{}) error {
identityEntityUpdateFields(d, data, false)

_, err := client.Logical().Write(path, data)

if err != nil {
return fmt.Errorf("error updating IdentityEntity %q: %s", id, err)
}
Expand All @@ -168,14 +170,15 @@ func identityEntityRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*api.Client)
id := d.Id()

resp, err := readIdentityEntity(client, id)
resp, err := readIdentityEntity(client, id, d.IsNewResource())
if err != nil {
// We need to check if the secret_id has expired
if util.IsExpiredTokenErr(err) {
if resp == nil && util.IsExpiredTokenErr(err) {
return nil
}
return fmt.Errorf("error reading IdentityEntity %q: %s", id, err)
}

log.Printf("[DEBUG] Read IdentityEntity %s", id)
if resp == nil {
log.Printf("[WARN] IdentityEntity %q not found, removing from state", id)
Expand Down Expand Up @@ -242,28 +245,48 @@ func identityEntityIDPath(id string) string {
}

func readIdentityEntityPolicies(client *api.Client, entityID string) ([]interface{}, error) {
resp, err := readIdentityEntity(client, entityID)
resp, err := readIdentityEntity(client, entityID, false)
if err != nil {
return nil, err
}
if resp == nil {
return nil, fmt.Errorf("error IdentityEntity %s does not exist", entityID)
}

if v, ok := resp.Data["policies"]; ok && v != nil {
return v.([]interface{}), nil
}
return make([]interface{}, 0), nil
}

// May return nil if entity does not exist
func readIdentityEntity(client *api.Client, entityID string) (*api.Secret, error) {
func readIdentityEntity(client *api.Client, entityID string, retry bool) (*api.Secret, error) {
path := identityEntityIDPath(entityID)
log.Printf("[DEBUG] Reading Entity %s from %q", entityID, path)
log.Printf("[DEBUG] Reading Entity %q from %q", entityID, path)

return readEntity(client, path, retry)
}

func readEntity(client *api.Client, path string, retry bool) (*api.Secret, error) {
log.Printf("[DEBUG] Reading Entity from %q", path)

var err error
if retry {
client, err = client.Clone()
if err != nil {
return nil, err
}
util.SetupCCCRetryClient(client, maxHTTPRetriesCCC)
}

resp, err := client.Logical().Read(path)
if err != nil {
return resp, fmt.Errorf("failed reading IdentityEntity %s from %s", entityID, path)
return resp, fmt.Errorf("failed reading %q", path)
}

if resp == nil {
return nil, fmt.Errorf("%w: %q", errEntityNotFound, path)
}

return resp, nil
}

func isIdentityNotFoundError(err error) bool {
return err != nil && errors.Is(err, errEntityNotFound)
}
5 changes: 3 additions & 2 deletions vault/resource_identity_entity_policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import (
"log"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-vault/util"
"github.com/hashicorp/vault/api"

"github.com/hashicorp/terraform-provider-vault/util"
)

func identityEntityPoliciesResource() *schema.Resource {
Expand Down Expand Up @@ -96,7 +97,7 @@ func identityEntityPoliciesRead(d *schema.ResourceData, meta interface{}) error
client := meta.(*api.Client)
id := d.Id()

resp, err := readIdentityEntity(client, id)
resp, err := readIdentityEntity(client, id, d.IsNewResource())
if err != nil {
return err
}
Expand Down
10 changes: 5 additions & 5 deletions vault/resource_identity_entity_policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ func testAccCheckidentityEntityPoliciesDestroy(s *terraform.State) error {
continue
}

entity, err := readIdentityEntity(client, rs.Primary.ID)
if err != nil {
if _, err := readIdentityEntity(client, rs.Primary.ID, false); err != nil {
if isIdentityNotFoundError(err) {
continue
}
return err
}
if entity == nil {
continue
}

apiPolicies, err := readIdentityEntityPolicies(client, rs.Primary.ID)
if err != nil {
return err
Expand Down
Loading