// 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/aws/request" "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" ) // 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 secretsmanageriface.SecretsManagerAPI, 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.DescribeSecretWithContext(ctx, input, request.WithAppendUserAgent(userAgent())) 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 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) } }