secretcache/cacheItem.go (158 lines of code) (raw):
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You
// may not use this file except in compliance with the License. A copy of
// the License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
// ANY KIND, either express or implied. See the License for the specific
// language governing permissions and limitations under the License.
package secretcache
import (
"context"
"fmt"
"math"
"math/rand"
"time"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
)
// secretCacheItem maintains a cache of secret versions.
type secretCacheItem struct {
versions *lruCache
// The next scheduled refresh time for this item. Once the item is accessed
// after this time, the item will be synchronously refreshed.
nextRefreshTime int64
*cacheObject
}
// newSecretCacheItem initialises a secretCacheItem using default cache size and sets next refresh time to now
func newSecretCacheItem(config CacheConfig, client SecretsManagerAPIClient, secretId string) secretCacheItem {
return secretCacheItem{
versions: newLRUCache(10),
cacheObject: &cacheObject{config: config, client: client, secretId: secretId, refreshNeeded: true},
nextRefreshTime: time.Now().UnixNano(),
}
}
// isRefreshNeeded determines if the cached item should be refreshed.
func (ci *secretCacheItem) isRefreshNeeded() bool {
if ci.cacheObject.isRefreshNeeded() {
return true
}
return ci.nextRefreshTime <= time.Now().UnixNano()
}
// getVersionId gets the version id for the given version stage.
// Returns the version id and a boolean to indicate success.
func (ci *secretCacheItem) getVersionId(versionStage string) (string, bool) {
result := ci.getWithHook()
if result == nil {
return "", false
}
if result.VersionIdsToStages == nil {
return "", false
}
for versionId, stages := range result.VersionIdsToStages {
for _, stage := range stages {
if versionStage == stage {
return versionId, true
}
}
}
return "", false
}
// executeRefresh performs the actual refresh of the cached secret information.
// Returns the DescribeSecret API result and an error if call failed.
func (ci *secretCacheItem) executeRefresh(ctx context.Context) (*secretsmanager.DescribeSecretOutput, error) {
input := &secretsmanager.DescribeSecretInput{
SecretId: &ci.secretId,
}
result, err := ci.client.DescribeSecret(ctx, input)
var maxTTL int64
if ci.config.CacheItemTTL == 0 {
maxTTL = DefaultCacheItemTTL
} else {
maxTTL = ci.config.CacheItemTTL
}
var ttl int64
if maxTTL < 0 {
return nil, &InvalidConfigError{
baseError{
Message: "cannot set negative ttl on cache",
},
}
} else if maxTTL < 2 {
ttl = maxTTL
} else {
ttl = rand.Int63n(maxTTL/2) + maxTTL/2
}
ci.nextRefreshTime = time.Now().Add(time.Nanosecond * time.Duration(ttl)).UnixNano()
return result, err
}
// getVersion gets the secret cache version associated with the given stage.
// Returns a boolean to indicate operation success.
func (ci *secretCacheItem) getVersion(versionStage string) (*cacheVersion, bool) {
versionId, versionIdFound := ci.getVersionId(versionStage)
if !versionIdFound {
return nil, false
}
cachedValue, cachedValueFound := ci.versions.get(versionId)
if !cachedValueFound {
cacheVersion := newCacheVersion(ci.config, ci.client, ci.secretId, versionId)
ci.versions.putIfAbsent(versionId, &cacheVersion)
cachedValue, _ = ci.versions.get(versionId)
}
secretCacheVersion, _ := cachedValue.(*cacheVersion)
return secretCacheVersion, true
}
// refresh the cached object on demand
func (ci *secretCacheItem) refreshNow(ctx context.Context) {
ci.refreshNeeded = true
// Generate a random number to have a sleep jitter to not get stuck in a retry loop
sleep := rand.Int63n((forceRefreshJitterSleep+1)-(forceRefreshJitterSleep/2)+1) + (forceRefreshJitterSleep / 2)
if ci.err != nil {
exceptionSleep := ci.nextRefreshTime - time.Now().UnixNano()
if exceptionSleep > sleep {
sleep = exceptionSleep
}
}
time.Sleep(time.Millisecond * time.Duration(sleep))
ci.refresh(ctx)
}
// refresh the cached object when needed.
func (ci *secretCacheItem) refresh(ctx context.Context) {
if !ci.isRefreshNeeded() {
return
}
ci.refreshNeeded = false
result, err := ci.executeRefresh(ctx)
if err != nil {
ci.errorCount++
ci.err = err
delay := exceptionRetryDelayBase * math.Pow(exceptionRetryGrowthFactor, float64(ci.errorCount))
delay = math.Min(delay, exceptionRetryDelayMax)
delayDuration := time.Millisecond * time.Duration(delay)
ci.nextRetryTime = time.Now().Add(delayDuration).UnixNano()
return
}
ci.setWithHook(result)
ci.err = nil
ci.errorCount = 0
}
// getSecretValue gets the cached secret value for the given version stage.
// Returns the GetSecretValue API result and an error if operation fails.
func (ci *secretCacheItem) getSecretValue(ctx context.Context, versionStage string) (*secretsmanager.GetSecretValueOutput, error) {
if versionStage == "" && ci.config.VersionStage == "" {
versionStage = DefaultVersionStage
} else if versionStage == "" && ci.config.VersionStage != "" {
versionStage = ci.config.VersionStage
}
ci.mux.Lock()
defer ci.mux.Unlock()
ci.refresh(ctx)
version, ok := ci.getVersion(versionStage)
if !ok {
if ci.err != nil {
return nil, ci.err
} else {
return nil, &VersionNotFoundError{
baseError{
Message: fmt.Sprintf("could not find secret version for versionStage %s", versionStage),
},
}
}
}
return version.getSecretValue(ctx)
}
// setWithHook sets the cache item's data using the CacheHook, if one is configured.
func (ci *secretCacheItem) setWithHook(result *secretsmanager.DescribeSecretOutput) {
if ci.config.Hook != nil {
ci.data = ci.config.Hook.Put(result)
} else {
ci.data = result
}
}
// getWithHook gets the cache item's data using the CacheHook, if one is configured.
func (ci *secretCacheItem) getWithHook() *secretsmanager.DescribeSecretOutput {
var result interface{}
if ci.config.Hook != nil {
result = ci.config.Hook.Get(ci.data)
} else {
result = ci.data
}
if result == nil {
return nil
} else {
return result.(*secretsmanager.DescribeSecretOutput)
}
}