diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e12e0a0..d60e6028 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
+### Added
+- Adds support for Secrets Provider secrets rotation feature, Community release.
+ [cyberark/secrets-provider-for-k8s#426](https://github.com/cyberark/secrets-provider-for-k8s/pull/426)
+
## [1.3.0] - 2022-01-03
### Added
diff --git a/PUSH_TO_FILE.md b/PUSH_TO_FILE.md
index 7f17b4f7..d0a0ee96 100644
--- a/PUSH_TO_FILE.md
+++ b/PUSH_TO_FILE.md
@@ -323,12 +323,14 @@ for a description of each environment variable setting:
| K8s Annotation | Equivalent
Environment Variable | Description, Notes |
|-----------------------------------------|---------------------|----------------------------------|
| `conjur.org/authn-identity` | `CONJUR_AUTHN_LOGIN` | Required value. Example: `host/conjur/authn-k8s/cluster/apps/inventory-api` |
-| `conjur.org/container-mode` | `CONTAINER_MODE` | Allowed values:
Defaults to `init`.
Must be set (or default) to `init` for Push to File mode.|
+| `conjur.org/container-mode` | `CONTAINER_MODE` | Allowed values: - `init`
- `application`
- `sidecar`
Defaults to `init`.
Must be set (or default) to `init` or `sidecar`for Push to File mode.|
| `conjur.org/secrets-destination` | `SECRETS_DESTINATION` | Allowed values: |
| `conjur.org/k8s-secrets` | `K8S_SECRETS` | This list is ignored when `conjur.org/secrets-destination` annotation is set to **`file`** |
| `conjur.org/retry-count-limit` | `RETRY_COUNT_LIMIT` | Defaults to 5
| `conjur.org/retry-interval-sec` | `RETRY_INTERVAL_SEC` | Defaults to 1 (sec) |
| `conjur.org/debug-logging` | `DEBUG` | Defaults to `false` |
+| `conjur.org/secrets-refresh-enabled`| Note\* | Defaults to `false` unless `conjur.org/secrets-rotation-interval` is explicitly set. Secrets Provider will exit with error if this is set to `false` and `conjur.org/secrets-rotation-interval` is set. |
+| `conjur.org/secrets-refresh-interval` | Note\* | Set to a valid duration string as defined [here](https://pkg.go.dev/time#ParseDuration). Setting a time implicitly enables refresh. Valid time units are `s`, `m`, and `h` (for seconds, minutes, and hours, respectively). Some examples of valid duration strings:The minimum refresh interval is 1 second. A refresh interval of 0 seconds is treated as a fatal configuration error. The default refresh interval is 5 minutes. The maximum refresh interval is approximately 290 years. |
| `conjur.org/conjur-secrets.{secret-group}` | Note\* | List of secrets to be retrieved from Conjur. Each entry can be either:- A Conjur variable path
- A key/value pairs of the form `:` where the `alias` represents the name of the secret to be written to the secrets file |
| `conjur.org/conjur-secrets-policy-path.{secret-group}` | Note\* | Defines a common Conjur policy path, assumed to be relative to the root policy.
When this annotation is set, the policy paths defined by `conjur.org/conjur-secrets.{secret-group}` are relative to this common path.
When this annotation is not set, the policy paths defined by `conjur.org/conjur-secrets.{secret-group}` are themselves relative to the root policy.
(See [Example Common Policy Path](#example-common-policy-path) for an explicit example of this relationship.)|
| `conjur.org/secret-file-path.{secret-group}` | Note\* | Relative path for secret file or directory to be written. This path is assumed to be relative to the respective mount path for the shared secrets volume for each container.
If the `conjur.org/secret-file-template.{secret-group}` is set, then this secret file path defaults to `{secret-group}.out`. For example, if the secret group name is `my-app`, the the secret file path defaults to `my-app.out`.
If the `conjur.org/secret-file-template.{secret-group}` is not set, then this secret file path defaults to `{secret-group}.{secret-group-file-format}`. For example, if the secret group name is `my-app`, and the secret file format is set for YAML, the the secret file path defaults to `my-app.yaml`.
diff --git a/cmd/secrets-provider/main.go b/cmd/secrets-provider/main.go
index 8307f8f3..5c268588 100644
--- a/cmd/secrets-provider/main.go
+++ b/cmd/secrets-provider/main.go
@@ -81,17 +81,15 @@ func main() {
}
// Gather secrets config and create a retryable Secrets Provider
- provideSecrets, secretsConfig, err := retryableSecretsProvider(ctx, tracer, secretRetriever)
+ provideSecrets, _, err := retryableSecretsProvider(ctx, tracer, secretRetriever)
if err != nil {
logError(err.Error())
return
}
// Provide secrets
- err = provideSecrets()
- if err != nil {
- errStr := fmt.Sprintf(messages.CSPFK039E, secretsConfig.StoreType, err.Error())
- logError(errStr)
+ if err = provideSecrets(); err != nil {
+ logError(err.Error())
}
}
@@ -131,11 +129,6 @@ func secretRetriever(ctx context.Context,
log.Error(messages.CSPFK008E)
return nil, err
}
- if err = validateContainerMode(authnConfig.GetContainerMode()); err != nil {
- span.RecordErrorAndSetStatus(err)
- log.Error(err.Error())
- return nil, err
- }
// Initialize a Conjur secret retriever
secretRetriever, err := conjur.NewSecretRetriever(authnConfig)
@@ -187,6 +180,20 @@ func retryableSecretsProvider(
secretsConfig.RetryCountLimit,
provideSecrets,
)
+
+ // Create a channel to send a quit signal to the periodic secret provider.
+ // TODO: Currently, this is just used for testing, but in the future we
+ // may want to create a SIGTERM or SIGHUP handler to catch a signal from
+ // a user / external entity, and then send an (empty struct) quit signal
+ // on this channel to trigger a graceful shut down of the Secrets Provider.
+ providerQuit := make(chan struct{})
+
+ provideSecrets = secrets.PeriodicSecretProvider(
+ secretsConfig.SecretsRefreshInterval,
+ getContainerMode(),
+ provideSecrets,
+ providerQuit,
+ )
return provideSecrets, secretsConfig, nil
}
@@ -233,16 +240,10 @@ func logErrorsAndInfos(errLogs []error, infoLogs []error) error {
return nil
}
-func validateContainerMode(containerMode string) error {
- validContainerModes := []string{
- "init",
- "application",
- }
-
- for _, validContainerModeType := range validContainerModes {
- if containerMode == validContainerModeType {
- return nil
- }
+func getContainerMode() string {
+ containerMode := "init"
+ if mode, exists := annotationsMap[secretsConfigProvider.ContainerModeKey]; exists {
+ containerMode = mode
}
- return fmt.Errorf(messages.CSPFK007E, containerMode, validContainerModes)
+ return containerMode
}
diff --git a/pkg/log/messages/error_messages.go b/pkg/log/messages/error_messages.go
index b28a4166..fa348006 100644
--- a/pkg/log/messages/error_messages.go
+++ b/pkg/log/messages/error_messages.go
@@ -20,7 +20,6 @@ const CSPFK003E string = "CSPFK003E AccessToken failed to delete access token da
// Environment variables
const CSPFK004E string = "CSPFK004E Environment variable '%s' must be provided"
const CSPFK005E string = "CSPFK005E Provided incorrect value for environment variable %s"
-const CSPFK007E string = "CSPFK007E CONTAINER_MODE '%s' is not supported. Supported values are: %v"
// Authenticator
const CSPFK008E string = "CSPFK008E Failed to instantiate authenticator configuration"
@@ -68,6 +67,8 @@ const CSPFK046E string = "CSPFK046E Secret Store Type needs to be configured, ei
const CSPFK047E string = "CSPFK047E Secrets Provider in Push-to-File mode can only be configured with Pod annotations"
const CSPFK048E string = "CSPFK048E Secrets Provider in K8s Secrets mode requires either the 'K8S_SECRETS' environment variable or 'conjur.org/k8s-secrets' Pod annotation"
const CSPFK049E string = "CSPFK049E Failed to validate Pod annotations"
+const CSPFK050E string = "CSPFK050E Invalid secrets refresh interval annotation: %s %s"
+const CSPFK051E string = "CSPFK050E Invalid secrets refresh configuration: %s %s"
// Push to File
const CSPFK053E string = "CSPFK053E Unable to initialize Secrets Provider: unable to create secret group collection"
diff --git a/pkg/secrets/config/config.go b/pkg/secrets/config/config.go
index c97811b4..c2418565 100644
--- a/pkg/secrets/config/config.go
+++ b/pkg/secrets/config/config.go
@@ -6,6 +6,7 @@ import (
"os"
"strconv"
"strings"
+ "time"
"github.com/cyberark/secrets-provider-for-k8s/pkg/log/messages"
)
@@ -19,7 +20,11 @@ const (
DefaultRetryCountLimit = 5
DefaultRetryIntervalSec = 1
MinRetryValue = 0
+ MinRefreshInterval = time.Second
+ DefaultRefreshIntervalStr = "5m"
+
)
+var DefaultRefreshInterval,_ = time.ParseDuration(DefaultRefreshIntervalStr)
// Config defines the configuration parameters
// for the authentication requests
@@ -29,6 +34,7 @@ type Config struct {
RetryCountLimit int
RetryIntervalSec int
StoreType string
+ SecretsRefreshInterval time.Duration
}
type annotationType int
@@ -46,18 +52,39 @@ type annotationRestraints struct {
allowedValues []string
}
+const (
+ AuthnIdentityKey = "conjur.org/authn-identity"
+ JwtTokenPath = "conjur.org/jwt-token-path"
+ ContainerModeKey = "conjur.org/container-mode"
+ SecretsDestinationKey = "conjur.org/secrets-destination"
+ k8sSecretsKey = "conjur.org/k8s-secrets"
+ retryCountLimitKey = "conjur.org/retry-count-limit"
+ retryIntervalSecKey = "conjur.org/retry-interval-sec"
+ // SecretsRefreshIntervalKey is the Annotation key for setting the interval
+ // for retrieving Conjur secrets and updating Kubernetes Secrets or
+ // application secret files if necessary.
+ SecretsRefreshIntervalKey = "conjur.org/secrets-refresh-interval"
+ // SecretsRefreshEnabledKey is the Annotation key for enabling the refresh
+ SecretsRefreshEnabledKey = "conjur.org/secrets-refresh-enabled"
+ debugLoggingKey = "conjur.org/debug-logging"
+ logTracesKey = "conjur.org/log-traces"
+ jaegerCollectorUrl = "conjur.org/jaeger-collector-url"
+)
+
// Define supported annotation keys for Secrets Provider config, as well as value restraints for each
var secretsProviderAnnotations = map[string]annotationRestraints{
- "conjur.org/authn-identity": {TYPESTRING, []string{}},
- "conjur.org/jwt-token-path": {TYPESTRING, []string{}},
- "conjur.org/container-mode": {TYPESTRING, []string{"init", "application"}},
- "conjur.org/secrets-destination": {TYPESTRING, []string{"file", "k8s_secrets"}},
- "conjur.org/k8s-secrets": {TYPESTRING, []string{}},
- "conjur.org/retry-count-limit": {TYPEINT, []string{}},
- "conjur.org/retry-interval-sec": {TYPEINT, []string{}},
- "conjur.org/debug-logging": {TYPEBOOL, []string{}},
- "conjur.org/log-traces": {TYPEBOOL, []string{}},
- "conjur.org/jaeger-collector-url": {TYPESTRING, []string{}},
+ AuthnIdentityKey: {TYPESTRING, []string{}},
+ JwtTokenPath: {TYPESTRING, []string{}},
+ ContainerModeKey: {TYPESTRING, []string{"init", "application", "sidecar"}},
+ SecretsDestinationKey: {TYPESTRING, []string{"file", "k8s_secrets"}},
+ k8sSecretsKey: {TYPESTRING, []string{}},
+ retryCountLimitKey: {TYPEINT, []string{}},
+ retryIntervalSecKey: {TYPEINT, []string{}},
+ SecretsRefreshIntervalKey: {TYPESTRING, []string{}},
+ SecretsRefreshEnabledKey: {TYPEBOOL, []string{}},
+ debugLoggingKey: {TYPEBOOL, []string{}},
+ logTracesKey: {TYPEBOOL, []string{}},
+ jaegerCollectorUrl: {TYPESTRING, []string{}},
}
// Define supported annotation key prefixes for Push to File config, as well as value restraints for each.
@@ -70,7 +97,7 @@ var pushToFileAnnotationPrefixes = map[string]annotationRestraints{
"conjur.org/secret-file-path.": {TYPESTRING, []string{}},
"conjur.org/secret-file-format.": {TYPESTRING, []string{"yaml", "json", "dotenv", "bash", "template"}},
"conjur.org/secret-file-permissions.": {TYPESTRING, []string{}},
- "conjur.org/secret-file-template": {TYPESTRING, []string{}},
+ "conjur.org/secret-file-template.": {TYPESTRING, []string{}},
}
// Define environment variables used in Secrets Provider config
@@ -140,51 +167,53 @@ func ValidateSecretsProviderSettings(envAndAnnots map[string]string) ([]error, [
}
envStoreType := envAndAnnots["SECRETS_DESTINATION"]
- annotStoreType := envAndAnnots["conjur.org/secrets-destination"]
+ annotStoreType := envAndAnnots[SecretsDestinationKey]
storeType := ""
+ var err error
- if annotStoreType == "" {
- switch envStoreType {
- case K8s:
- storeType = envStoreType
- case File:
- errorList = append(errorList, errors.New(messages.CSPFK047E))
- case "":
- errorList = append(errorList, errors.New(messages.CSPFK046E))
- default:
- errorList = append(errorList, fmt.Errorf(messages.CSPFK005E, "SECRETS_DESTINATION"))
+ switch {
+ case annotStoreType == "":
+ storeType, err = validateStore(envStoreType)
+ if err != nil {
+ errorList = append(errorList, err)
}
- } else if validStoreType(annotStoreType) {
+ case validStoreType(annotStoreType):
if validStoreType(envStoreType) {
- infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "StoreType", "SECRETS_DESTINATION", "conjur.org/secrets-destination"))
+ infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "StoreType", "SECRETS_DESTINATION", SecretsDestinationKey))
}
storeType = annotStoreType
- } else {
- errorList = append(errorList, fmt.Errorf(messages.CSPFK043E, "conjur.org/secrets-destination", annotStoreType, []string{File, K8s}))
+ default:
+ errorList = append(errorList, fmt.Errorf(messages.CSPFK043E, SecretsDestinationKey, annotStoreType, []string{File, K8s}))
}
-
envK8sSecretsStr := envAndAnnots["K8S_SECRETS"]
- annotK8sSecretsStr := envAndAnnots["conjur.org/k8s-secrets"]
+ annotK8sSecretsStr := envAndAnnots[k8sSecretsKey]
if storeType == "k8s_secrets" {
if envK8sSecretsStr == "" && annotK8sSecretsStr == "" {
errorList = append(errorList, errors.New(messages.CSPFK048E))
} else if envK8sSecretsStr != "" && annotK8sSecretsStr != "" {
- infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RequiredK8sSecrets", "K8S_SECRETS", "conjur.org/k8s-secrets"))
+ infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RequiredK8sSecrets", "K8S_SECRETS", k8sSecretsKey))
}
}
- annotRetryCountLimit := envAndAnnots["conjur.org/retry-count-limit"]
+ annotRetryCountLimit := envAndAnnots[retryCountLimitKey]
envRetryCountLimit := envAndAnnots["RETRY_COUNT_LIMIT"]
if annotRetryCountLimit != "" && envRetryCountLimit != "" {
- infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RetryCountLimit", "RETRY_COUNT_LIMIT", "conjur.org/retry-count-limit"))
+ infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RetryCountLimit", "RETRY_COUNT_LIMIT", retryCountLimitKey))
}
- annotRetryIntervalSec := envAndAnnots["conjur.org/retry-interval-sec"]
+ annotRetryIntervalSec := envAndAnnots[retryIntervalSecKey]
envRetryIntervalSec := envAndAnnots["RETRY_INTERVAL_SEC"]
if annotRetryIntervalSec != "" && envRetryIntervalSec != "" {
- infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RetryIntervalSec", "RETRY_INTERVAL_SEC", "conjur.org/retry-interval-sec"))
+ infoList = append(infoList, fmt.Errorf(messages.CSPFK012I, "RetryIntervalSec", "RETRY_INTERVAL_SEC", retryIntervalSecKey))
}
+ annotSecretsRefreshEnable := envAndAnnots[SecretsRefreshEnabledKey]
+ annotSecretsRefreshInterval := envAndAnnots[SecretsRefreshIntervalKey]
+ annotContainerMode := envAndAnnots[ContainerModeKey]
+ err = validRefreshInterval(annotSecretsRefreshInterval, annotSecretsRefreshEnable, annotContainerMode)
+ if err != nil {
+ errorList = append(errorList, err)
+ }
return errorList, infoList
}
@@ -193,14 +222,14 @@ func ValidateSecretsProviderSettings(envAndAnnots map[string]string) ([]error, [
func NewConfig(settings map[string]string) *Config {
podNamespace := settings["MY_POD_NAMESPACE"]
- storeType := settings["conjur.org/secrets-destination"]
+ storeType := settings[SecretsDestinationKey]
if storeType == "" {
storeType = settings["SECRETS_DESTINATION"]
}
k8sSecretsArr := []string{}
if storeType != "file" {
- k8sSecretsStr := settings["conjur.org/k8s-secrets"]
+ k8sSecretsStr := settings[k8sSecretsKey]
if k8sSecretsStr != "" {
k8sSecretsStr := strings.ReplaceAll(k8sSecretsStr, "- ", "")
k8sSecretsArr = strings.Split(k8sSecretsStr, "\n")
@@ -212,24 +241,34 @@ func NewConfig(settings map[string]string) *Config {
}
}
- retryCountLimitStr := settings["conjur.org/retry-count-limit"]
+ retryCountLimitStr := settings[retryCountLimitKey]
if retryCountLimitStr == "" {
retryCountLimitStr = settings["RETRY_COUNT_LIMIT"]
}
retryCountLimit := parseIntFromStringOrDefault(retryCountLimitStr, DefaultRetryCountLimit, MinRetryValue)
- retryIntervalSecStr := settings["conjur.org/retry-interval-sec"]
+ retryIntervalSecStr := settings[retryIntervalSecKey]
if retryIntervalSecStr == "" {
retryIntervalSecStr = settings["RETRY_INTERVAL_SEC"]
}
retryIntervalSec := parseIntFromStringOrDefault(retryIntervalSecStr, DefaultRetryIntervalSec, MinRetryValue)
+ refreshIntervalStr := settings[SecretsRefreshIntervalKey]
+ refreshEnableStr := settings[SecretsRefreshEnabledKey]
+
+ if refreshIntervalStr == "" && refreshEnableStr == "true" {
+ refreshIntervalStr = DefaultRefreshIntervalStr
+ }
+ // ignore errors here, if the interval string is null, zero is returned
+ refreshInterval, _ := time.ParseDuration(refreshIntervalStr)
+
return &Config{
PodNamespace: podNamespace,
RequiredK8sSecrets: k8sSecretsArr,
RetryCountLimit: retryCountLimit,
RetryIntervalSec: retryIntervalSec,
StoreType: storeType,
+ SecretsRefreshInterval: refreshInterval,
}
}
@@ -306,3 +345,47 @@ func validStoreType(storeType string) bool {
}
return false
}
+
+func validateStore(envStoreType string) (string,error) {
+ var err error
+ storeType := ""
+ switch envStoreType {
+ case K8s:
+ storeType = envStoreType
+ case File:
+ err = errors.New(messages.CSPFK047E)
+ case "":
+ err = errors.New(messages.CSPFK046E)
+ default:
+ err = fmt.Errorf(messages.CSPFK005E, "SECRETS_DESTINATION")
+ }
+ return storeType, err
+}
+
+func validRefreshInterval(intervalStr string, enableStr string, containerMode string) error {
+
+ var err error
+ if intervalStr != "" || enableStr != "" {
+ if containerMode != "sidecar" {
+ return fmt.Errorf(messages.CSPFK051E, "Secrets refresh is enabled while container mode is set to", containerMode)
+ }
+ enabled, _ := strconv.ParseBool(enableStr)
+ duration, e := time.ParseDuration(intervalStr)
+ switch {
+ // the user set enabled to true and did not set the interval
+ case intervalStr == "" && enableStr != "":
+ err = nil
+ // duration can't be parsed
+ case e != nil:
+ err = fmt.Errorf(messages.CSPFK050E, intervalStr, e.Error())
+ // check if the user explicitly set enable to false
+ case enabled == false && enableStr != "" && intervalStr != "":
+ err = fmt.Errorf(messages.CSPFK050E, intervalStr, "Secrets refresh interval set to value while enable is false")
+ // duration too small
+ case duration < MinRefreshInterval:
+ err = fmt.Errorf(messages.CSPFK050E, intervalStr, "Secrets refresh interval must be at least one second")
+ }
+ }
+ return err
+}
+
diff --git a/pkg/secrets/config/config_test.go b/pkg/secrets/config/config_test.go
index 4bbbe99f..40479d81 100644
--- a/pkg/secrets/config/config_test.go
+++ b/pkg/secrets/config/config_test.go
@@ -53,12 +53,13 @@ var validateAnnotationsTestCases = []validateAnnotationsTestCase{
{
description: "given properly formatted annotations, no error or info logs are returned",
annotations: map[string]string{
- "conjur.org/authn-identity": "host/conjur/authn-k8s/cluster/apps/inventory-api",
+ AuthnIdentityKey: "host/conjur/authn-k8s/cluster/apps/inventory-api",
"conjur.org/container-mode": "init",
"conjur.org/secret-destination": "file",
- "conjur.org/k8s-secrets": "- secret-1\n- secret-2\n- secret-3\n",
- "conjur.org/retry-count-limit": "12",
- "conjur.org/retry-interval-sec": "2",
+ k8sSecretsKey: "- secret-1\n- secret-2\n- secret-3\n",
+ retryCountLimitKey: "12",
+ retryIntervalSecKey: "2",
+ "conjur.org/secrets-refresh-interval": "5s",
"conjur.org/conjur-secrets.this-group": "- test/url\n- test-password: test/password\n- test-username: test/username\n",
"conjur.org/secret-file-path.this-group": "this-relative-path",
"conjur.org/secret-file-format.this-group": "yaml",
@@ -82,23 +83,23 @@ var validateAnnotationsTestCases = []validateAnnotationsTestCase{
{
description: "if an annotation is configured with an invalid value, an error is returned",
annotations: map[string]string{
- "conjur.org/secrets-destination": "invalid",
+ SecretsDestinationKey: "invalid",
},
- assert: assertErrorInList(fmt.Errorf(messages.CSPFK043E, "conjur.org/secrets-destination", "invalid", []string{"file", "k8s_secrets"})),
+ assert: assertErrorInList(fmt.Errorf(messages.CSPFK043E, SecretsDestinationKey, "invalid", []string{"file", "k8s_secrets"})),
},
{
description: "when an annotation expects an integer but is given a non-integer value, an error is returned",
annotations: map[string]string{
- "conjur.org/retry-count-limit": "seven",
+ retryCountLimitKey: "seven",
},
- assert: assertErrorInList(fmt.Errorf(messages.CSPFK042E, "conjur.org/retry-count-limit", "seven", "Integer")),
+ assert: assertErrorInList(fmt.Errorf(messages.CSPFK042E, retryCountLimitKey, "seven", "Integer")),
},
{
description: "when an annotation expects a boolean but is given a non-boolean value, an error is returned",
annotations: map[string]string{
- "conjur.org/debug-logging": "not-a-boolean",
+ debugLoggingKey: "not-a-boolean",
},
- assert: assertErrorInList(fmt.Errorf(messages.CSPFK042E, "conjur.org/debug-logging", "not-a-boolean", "Boolean")),
+ assert: assertErrorInList(fmt.Errorf(messages.CSPFK042E, debugLoggingKey, "not-a-boolean", "Boolean")),
},
}
@@ -113,8 +114,8 @@ var gatherSecretsProviderSettingsTestCases = []gatherSecretsProviderSettingsTest
{
description: "the resulting map will those annotations and envvars pertaining to Secrets Provider config",
annotations: map[string]string{
- "conjur.org/secrets-destination": "file",
- "conjur.org/container-mode": "init",
+ SecretsDestinationKey: "file",
+ "conjur.org/container-mode": "init",
},
env: map[string]string{
"SECRETS_DESTINATION": "file",
@@ -122,10 +123,10 @@ var gatherSecretsProviderSettingsTestCases = []gatherSecretsProviderSettingsTest
"UNRELATED_ENVVAR": "UNRELATED",
},
assert: assertGoodMap(map[string]string{
- "conjur.org/secrets-destination": "file",
- "conjur.org/container-mode": "init",
- "SECRETS_DESTINATION": "file",
- "RETRY_COUNT_LIMIT": "5",
+ SecretsDestinationKey: "file",
+ "conjur.org/container-mode": "init",
+ "SECRETS_DESTINATION": "file",
+ "RETRY_COUNT_LIMIT": "5",
}),
},
{
@@ -149,21 +150,21 @@ var gatherSecretsProviderSettingsTestCases = []gatherSecretsProviderSettingsTest
{
description: "given an empty environment, the returned map should contain the annotations",
annotations: map[string]string{
- "conjur.org/secrets-destination": "file",
- "conjur.org/container-mode": "init",
+ SecretsDestinationKey: "file",
+ "conjur.org/container-mode": "init",
},
env: map[string]string{},
assert: assertGoodMap(map[string]string{
- "conjur.org/secrets-destination": "file",
- "conjur.org/container-mode": "init",
+ SecretsDestinationKey: "file",
+ "conjur.org/container-mode": "init",
}),
},
{
description: "annotations and envvars not related to Secrets Provider config are omitted",
annotations: map[string]string{
- "conjur.org/secrets-destination": "file",
- "conjur.org/container-mode": "init",
- "conjur.org/unrelated-annot": "unrelated-value",
+ SecretsDestinationKey: "file",
+ "conjur.org/container-mode": "init",
+ "conjur.org/unrelated-annot": "unrelated-value",
},
env: map[string]string{
"MY_POD_NAMESPACE": "test-namespace",
@@ -171,7 +172,7 @@ var gatherSecretsProviderSettingsTestCases = []gatherSecretsProviderSettingsTest
"UNRELATED_ENVVAR": "unrelated-value",
},
assert: assertGoodMap(map[string]string{
- "conjur.org/secrets-destination": "file",
+ SecretsDestinationKey: "file",
"conjur.org/container-mode": "init",
"MY_POD_NAMESPACE": "test-namespace",
"RETRY_COUNT_LIMIT": "5",
@@ -189,11 +190,14 @@ var validateSecretsProviderSettingsTestCases = []validateSecretsProviderSettings
{
description: "given a valid configuration of annotations, no errors are returned",
envAndAnnots: map[string]string{
- "MY_POD_NAMESPACE": "test-namespace",
- "conjur.org/secrets-destination": "file",
- "conjur.org/retry-count-limit": "10",
- "conjur.org/retry-interval-sec": "20",
- "conjur.org/k8s-secrets": "- secret-1\n- secret-2\n- secret-3\n",
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsDestinationKey: "file",
+ retryCountLimitKey: "10",
+ retryIntervalSecKey: "20",
+ k8sSecretsKey: "- secret-1\n- secret-2\n- secret-3\n",
+ "conjur.org/container-mode": "sidecar",
+ "conjur.org/secrets-refresh-interval": "5m",
+ "conjur.org/secrets-refresh-enabled": "true",
},
assert: assertEmptyErrorList(),
},
@@ -211,52 +215,52 @@ var validateSecretsProviderSettingsTestCases = []validateSecretsProviderSettings
{
description: "mixed-source config",
envAndAnnots: map[string]string{
- "MY_POD_NAMESPACE": "test-namespace",
- "conjur.org/secrets-destination": "k8s_secrets",
- "RETRY_COUNT_LIMIT": "10",
- "conjur.org/retry-interval-sec": "20",
- "K8S_SECRETS": "secret-1,secret-2,secret-3",
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsDestinationKey: "k8s_secrets",
+ "RETRY_COUNT_LIMIT": "10",
+ retryIntervalSecKey: "20",
+ "K8S_SECRETS": "secret-1,secret-2,secret-3",
},
assert: assertEmptyErrorList(),
},
{
description: "if StoreType is configured with both its annotation and envVar, an info-level error is returned",
envAndAnnots: map[string]string{
- "MY_POD_NAMESPACE": "test-namespace",
- "SECRETS_DESTINATION": "k8s_secrets",
- "conjur.org/secrets-destination": "file",
+ "MY_POD_NAMESPACE": "test-namespace",
+ "SECRETS_DESTINATION": "k8s_secrets",
+ SecretsDestinationKey: "file",
},
- assert: assertInfoInList(fmt.Errorf(messages.CSPFK012I, "StoreType", "SECRETS_DESTINATION", "conjur.org/secrets-destination")),
+ assert: assertInfoInList(fmt.Errorf(messages.CSPFK012I, "StoreType", "SECRETS_DESTINATION", SecretsDestinationKey)),
},
{
description: "if RequiredK8sSecrets is configured with both its annotation and envVar, an info-level error is returned",
envAndAnnots: map[string]string{
- "MY_POD_NAMESPACE": "test-namespace",
- "conjur.org/secrets-destination": "k8s_secrets",
- "conjur.org/k8s-secrets": "- secret-1\n- secret-2\n",
- "K8S_SECRETS": "another-secret-1,another-secret-2",
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsDestinationKey: "k8s_secrets",
+ k8sSecretsKey: "- secret-1\n- secret-2\n",
+ "K8S_SECRETS": "another-secret-1,another-secret-2",
},
- assert: assertInfoInList(fmt.Errorf(messages.CSPFK012I, "RequiredK8sSecrets", "K8S_SECRETS", "conjur.org/k8s-secrets")),
+ assert: assertInfoInList(fmt.Errorf(messages.CSPFK012I, "RequiredK8sSecrets", "K8S_SECRETS", k8sSecretsKey)),
},
{
description: "if RetryCountLimit is configured with both its annotation and envVar, an info-level error is returned",
envAndAnnots: map[string]string{
- "MY_POD_NAMESPACE": "test-namespace",
- "conjur.org/secrets-destination": "file",
- "conjur.org/retry-count-limit": "10",
- "RETRY_COUNT_LIMIT": "12",
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsDestinationKey: "file",
+ retryCountLimitKey: "10",
+ "RETRY_COUNT_LIMIT": "12",
},
- assert: assertInfoInList(fmt.Errorf(messages.CSPFK012I, "RetryCountLimit", "RETRY_COUNT_LIMIT", "conjur.org/retry-count-limit")),
+ assert: assertInfoInList(fmt.Errorf(messages.CSPFK012I, "RetryCountLimit", "RETRY_COUNT_LIMIT", retryCountLimitKey)),
},
{
description: "if RetryIntervalSec is configured with both its annotation and envVar, an info-level error is returned",
envAndAnnots: map[string]string{
- "MY_POD_NAMESPACE": "test-namespace",
- "conjur.org/secrets-destination": "file",
- "conjur.org/retry-interval-sec": "2",
- "RETRY_INTERVAL_SEC": "7",
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsDestinationKey: "file",
+ retryIntervalSecKey: "2",
+ "RETRY_INTERVAL_SEC": "7",
},
- assert: assertInfoInList(fmt.Errorf(messages.CSPFK012I, "RetryIntervalSec", "RETRY_INTERVAL_SEC", "conjur.org/retry-interval-sec")),
+ assert: assertInfoInList(fmt.Errorf(messages.CSPFK012I, "RetryIntervalSec", "RETRY_INTERVAL_SEC", retryIntervalSecKey)),
},
{
description: "if MY_POD_NAMESPACE envVar is not set, an error is returned",
@@ -281,16 +285,16 @@ var validateSecretsProviderSettingsTestCases = []validateSecretsProviderSettings
{
description: "if 'conjur.org/secrets-destination' is provided and malformed, an error is returned",
envAndAnnots: map[string]string{
- "MY_POD_NAMESPACE": "test-namespace",
- "conjur.org/secrets-destination": "invalid",
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsDestinationKey: "invalid",
},
- assert: assertErrorInList(fmt.Errorf(messages.CSPFK043E, "conjur.org/secrets-destination", "invalid", []string{File, K8s})),
+ assert: assertErrorInList(fmt.Errorf(messages.CSPFK043E, SecretsDestinationKey, "invalid", []string{File, K8s})),
},
{
description: "if RequiredK8sSecrets is not configured in K8s Secrets mode, an error is returned",
envAndAnnots: map[string]string{
- "MY_POD_NAMESPACE": "test-namespace",
- "conjur.org/secrets-destination": "k8s_secrets",
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsDestinationKey: "k8s_secrets",
},
assert: assertErrorInList(errors.New(messages.CSPFK048E)),
},
@@ -311,6 +315,91 @@ var validateSecretsProviderSettingsTestCases = []validateSecretsProviderSettings
},
assert: assertErrorInList(fmt.Errorf(messages.CSPFK005E, "SECRETS_DESTINATION")),
},
+ {
+ description: "if refresh interval is malformed, an error is returned",
+ envAndAnnots: map[string]string{
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsRefreshIntervalKey: "5",
+ ContainerModeKey: "sidecar",
+ },
+ assert: assertErrorInList(fmt.Errorf(messages.CSPFK050E, "5", "time: missing unit in duration \"5\"")),
+ },
+ {
+ description: "if refresh interval is malformed with enable set to true, an error is returned",
+ envAndAnnots: map[string]string{
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsRefreshEnabledKey: "true",
+ SecretsRefreshIntervalKey: "5",
+ ContainerModeKey: "sidecar",
+ },
+ assert: assertErrorInList(fmt.Errorf(messages.CSPFK050E, "5", "time: missing unit in duration \"5\"")),
+ },
+ {
+ description: "if refresh enable is true and interval not set, no errors are returned",
+ envAndAnnots: map[string]string{
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsDestinationKey: "file",
+ SecretsRefreshEnabledKey: "true",
+ ContainerModeKey: "sidecar",
+ },
+ assert: assertEmptyErrorList(),
+ },
+ {
+ description: "if refresh enable is false and interval not set, no errors are returned",
+ envAndAnnots: map[string]string{
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsDestinationKey: "file",
+ SecretsRefreshEnabledKey: "false",
+ ContainerModeKey: "sidecar",
+ },
+ assert: assertEmptyErrorList(),
+ },
+ {
+ description: "if refresh interval is zero, an error is returned",
+ envAndAnnots: map[string]string{
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsRefreshIntervalKey: "0",
+ ContainerModeKey: "sidecar",
+ },
+ assert: assertErrorInList(fmt.Errorf(messages.CSPFK050E, "0", "Secrets refresh interval must be at least one second")),
+ },
+ {
+ description: "if refresh interval is too small, an error is returned",
+ envAndAnnots: map[string]string{
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsRefreshIntervalKey: "500ms",
+ ContainerModeKey: "sidecar",
+ },
+ assert: assertErrorInList(fmt.Errorf(messages.CSPFK050E, "500ms", "Secrets refresh interval must be at least one second")),
+ },
+ {
+ description: "if refresh interval is negative, an error is returned",
+ envAndAnnots: map[string]string{
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsRefreshIntervalKey: "-5s",
+ ContainerModeKey: "sidecar",
+ },
+ assert: assertErrorInList(fmt.Errorf(messages.CSPFK050E, "-5s", "Secrets refresh interval must be at least one second")),
+ },
+ {
+ description: "if refresh interval is set and enable is false",
+ envAndAnnots: map[string]string{
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsRefreshIntervalKey: "5s",
+ SecretsRefreshEnabledKey: "false",
+ ContainerModeKey: "sidecar",
+ },
+ assert: assertErrorInList(fmt.Errorf(messages.CSPFK050E, "5s", "Secrets refresh interval set to value while enable is false")),
+ },
+ {
+ description: "if refresh interval is set and container mode is init",
+ envAndAnnots: map[string]string{
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsRefreshIntervalKey: "5s",
+ ContainerModeKey: "init",
+ },
+ assert: assertErrorInList(fmt.Errorf(messages.CSPFK051E, "Secrets refresh is enabled while container mode is set to", "init")),
+ },
}
type newConfigTestCase struct {
@@ -324,10 +413,10 @@ var newConfigTestCases = []newConfigTestCase{
description: "a valid map of annotation-based Secrets Provider settings returns a valid Config",
settings: map[string]string{
"MY_POD_NAMESPACE": "test-namespace",
- "conjur.org/secrets-destination": "k8s_secrets",
- "conjur.org/k8s-secrets": "- secret-1\n- secret-2\n- secret-3\n",
- "conjur.org/retry-count-limit": "10",
- "conjur.org/retry-interval-sec": "20",
+ SecretsDestinationKey: "k8s_secrets",
+ k8sSecretsKey: "- secret-1\n- secret-2\n- secret-3\n",
+ retryCountLimitKey: "10",
+ retryIntervalSecKey: "20",
},
assert: assertGoodConfig(&Config{
PodNamespace: "test-namespace",
@@ -359,7 +448,7 @@ var newConfigTestCases = []newConfigTestCase{
settings: map[string]string{
"MY_POD_NAMESPACE": "test-namespace",
"SECRETS_DESTINATION": "k8s_secrets",
- "conjur.org/secrets-destination": "file",
+ SecretsDestinationKey: "file",
"K8S_SECRETS": "secret-1,secret-2,secret-3",
},
assert: assertGoodConfig(&Config{
@@ -374,9 +463,9 @@ var newConfigTestCases = []newConfigTestCase{
description: "mixed-source config",
settings: map[string]string{
"MY_POD_NAMESPACE": "test-namespace",
- "conjur.org/secrets-destination": "k8s_secrets",
+ SecretsDestinationKey: "k8s_secrets",
"RETRY_COUNT_LIMIT": "10",
- "conjur.org/retry-interval-sec": "20",
+ retryIntervalSecKey: "20",
"K8S_SECRETS": "secret-1,secret-2,secret-3",
},
assert: assertGoodConfig(&Config{
@@ -387,6 +476,22 @@ var newConfigTestCases = []newConfigTestCase{
RetryIntervalSec: 20,
}),
},
+ {
+ description: "a valid map of annotation-based settings with refresh enabled returns a valid Config",
+ settings: map[string]string{
+ "MY_POD_NAMESPACE": "test-namespace",
+ SecretsDestinationKey: "file",
+ SecretsRefreshEnabledKey: "true",
+ },
+ assert: assertGoodConfig(&Config{
+ PodNamespace: "test-namespace",
+ StoreType: "file",
+ RequiredK8sSecrets: []string{},
+ RetryCountLimit: 5,
+ RetryIntervalSec: 1,
+ SecretsRefreshInterval: DefaultRefreshInterval,
+ }),
+ },
}
func TestValidateAnnotations(t *testing.T) {
diff --git a/pkg/secrets/provide_conjur_secrets.go b/pkg/secrets/provide_conjur_secrets.go
index 0799538d..9a4822ba 100644
--- a/pkg/secrets/provide_conjur_secrets.go
+++ b/pkg/secrets/provide_conjur_secrets.go
@@ -16,6 +16,10 @@ import (
"github.com/cyberark/secrets-provider-for-k8s/pkg/utils"
)
+const (
+ secretProviderGracePeriod = time.Duration(10 * time.Millisecond)
+)
+
// ProviderConfig provides the configuration necessary to create a secrets
// Provider.
type ProviderConfig struct {
@@ -87,7 +91,6 @@ func RetryableSecretProvider(
if limitedBackOff.RetryCount() > 0 {
log.Info(fmt.Sprintf(messages.CSPFK010I, limitedBackOff.RetryCount(), limitedBackOff.RetryLimit))
}
-
return provideSecrets()
}, limitedBackOff)
@@ -97,3 +100,77 @@ func RetryableSecretProvider(
return err
}
}
+
+// PeriodicSecretProvider returns a new ProviderFunc, which wraps a retryable
+// ProviderFunc inside a function that operates in one of three modes:
+// - Run once and return (for init or application container modes)
+// - Run once and sleep forever (for sidecar mode without periodic refresh)
+// - Run periodically (for sidecar mode with periodic refresh)
+func PeriodicSecretProvider(
+ secretRefreshInterval time.Duration,
+ mode string,
+ provideSecrets ProviderFunc,
+ providerQuit chan struct{},
+) ProviderFunc {
+
+ var periodicQuit = make(chan struct{})
+ var periodicError = make(chan error)
+ var ticker *time.Ticker
+
+ return func() error {
+ err := provideSecrets()
+ switch {
+ case err != nil:
+ // Return immediately upon error, regardless of operating mode
+ return err
+ case mode != "sidecar":
+ // Run once and return if not in sidecar mode
+ return err
+ case secretRefreshInterval > 0:
+ // Run periodically if in sidecar mode with periodic refresh
+ ticker = time.NewTicker(secretRefreshInterval)
+ go periodicSecretProvider(provideSecrets, ticker,
+ periodicQuit, periodicError)
+ default:
+ // Run once and sleep forever if in sidecar mode without
+ // periodic refresh (fall through)
+ }
+
+ // Wait here for a signal to quit providing secrets or an error
+ // from the periodicSecretProvider() function
+ select {
+ case <-providerQuit:
+ break
+ case err = <-periodicError:
+ break
+ }
+
+ // Allow the periodicSecretProvider goroutine to gracefully shut down
+ if secretRefreshInterval > 0 {
+ // Kill the ticker
+ ticker.Stop()
+ periodicQuit <- struct{}{}
+ time.Sleep(secretProviderGracePeriod)
+ }
+ return err
+ }
+}
+
+func periodicSecretProvider(
+ provideSecrets ProviderFunc,
+ ticker *time.Ticker,
+ periodicQuit <-chan struct{},
+ periodicError chan<- error,
+) {
+ for {
+ select {
+ case <-periodicQuit:
+ return
+ case <-ticker.C:
+ err := provideSecrets()
+ if err != nil {
+ periodicError <- err
+ }
+ }
+ }
+}
diff --git a/pkg/secrets/provide_conjur_secrets_test.go b/pkg/secrets/provide_conjur_secrets_test.go
index bb23e6ff..3410b874 100644
--- a/pkg/secrets/provide_conjur_secrets_test.go
+++ b/pkg/secrets/provide_conjur_secrets_test.go
@@ -14,11 +14,69 @@ import (
"github.com/stretchr/testify/assert"
)
+// TODO: For each test case, we need to create an object with its own
+// call count state, so that test cases don't interfere with one another.
+//type mockProvider struct {
+// calledCount int
+// callLatencyMsecs time.Duration
+// injectFailure bool
+// failOnCountN int
+//}
+
+//func (m mockProvider) provide() error {
+// m.calledCount++
+// if m.injectFailure && (m.calledCount >= m.failOnCountN) {
+// return errors.New("Failed to Provide")
+// }
+// if m.callLatencyMsecs > 0 {
+// time.Sleep(m.callLatencyMsecs * time.Millisecond)
+// }
+// return nil
+//}
+
+//func goodProvider() mockProvider {
+// return mockProvider{}
+//}
+
+//func badProvider() mockProvider {
+// return mockProvider{injectFailure: true, failOnCountN: 1}
+//}
+
+//func goodAtFirstThenBadProvider(failOnCountN int) mockProvider {
+// return mockProvider{injectFailure: true, failOnCountN: failOnCountN}
+//}
+
+//func slowProvider(latencyMsecs time.Duration) mockProvider {
+// return mockProvider{callLatencyMsecs: latencyMsecs}
+//}
+
+const (
+ providerDelayMsecs = 50
+)
+
+// TODO: Don't use global variable here. Create individual test case objects.
+var providerCount = 0
+
func badProvider() error {
return errors.New("Failed to Provide")
}
+func goodAtFirstThenBadProvider() error {
+ providerCount++
+ if providerCount > 2 {
+ return errors.New("Failed to Provide")
+ }
+ return nil
+}
+
func goodProvider() error {
+ providerCount++
+ return nil
+}
+
+func slowProvider() error {
+ providerCount++
+ time.Sleep(providerDelayMsecs * time.Millisecond)
return nil
}
@@ -97,3 +155,140 @@ func TestRetryableSecretProvider(t *testing.T) {
}
}
}
+
+func TestPeriodicSecretProvider(t *testing.T) {
+ TestCases := []struct {
+ description string
+ mode string
+ interval time.Duration
+ testTime time.Duration // total test time for all tests must be less than 3m
+ expectedCount int
+ provider ProviderFunc
+ assertOn string
+ }{
+ {
+ description: "init container, happy path",
+ mode: "init",
+ interval: time.Duration(10) * time.Millisecond,
+ testTime: time.Duration(25) * time.Millisecond,
+ expectedCount: 1,
+ provider: goodProvider,
+ assertOn: "success",
+ },
+ {
+ description: "sidecar container, happy path",
+ mode: "sidecar",
+ interval: time.Duration(10) * time.Millisecond,
+ testTime: time.Duration(65) * time.Millisecond,
+ expectedCount: 7,
+ provider: goodProvider,
+ assertOn: "success",
+ },
+ {
+ description: "sidecar with zero duration",
+ mode: "sidecar",
+ interval: time.Duration(0) * time.Millisecond,
+ testTime: time.Duration(25) * time.Millisecond,
+ expectedCount: 1,
+ provider: goodProvider,
+ assertOn: "success",
+ },
+ {
+ description: "application mode, happy path",
+ mode: "application",
+ interval: time.Duration(10) * time.Millisecond,
+ testTime: time.Duration(25) * time.Millisecond,
+ expectedCount: 1,
+ provider: goodProvider,
+ assertOn: "success",
+ },
+ {
+ description: "sidecar with slow provider",
+ mode: "sidecar",
+ interval: time.Duration(10) * time.Millisecond,
+ testTime: time.Duration(175) * time.Millisecond,
+ expectedCount: (150 / providerDelayMsecs) + 1,
+ provider: slowProvider,
+ assertOn: "success",
+ },
+ // This test is inconsistent, 11-13 ticks
+ //{
+ // In this test the provider takes longer to run than the
+ // interval time. The Go ticker will adjust the time interval due to the
+ // slower receiver.
+ //description: "sidecar with duration less than fetch time",
+ //mode: "sidecar",
+ //interval: time.Duration(10) * time.Millisecond,
+ //testTime: time.Duration(375) * time.Millisecond,
+ //expectedCount: 350/providerDelayMsecs + 1,
+ //provider: slowProvider,
+ //assertOn: "success",
+ //},
+ {
+ description: "badProvider for init container",
+ mode: "init",
+ interval: time.Duration(10) * time.Millisecond,
+ testTime: time.Duration(25) * time.Millisecond,
+ expectedCount: 1,
+ provider: badProvider,
+ assertOn: "fail",
+ },
+ {
+ description: "badProvider for sidecar",
+ mode: "sidecar",
+ interval: time.Duration(10) * time.Millisecond,
+ testTime: time.Duration(25) * time.Millisecond,
+ expectedCount: 1,
+ provider: badProvider,
+ assertOn: "fail",
+ },
+ {
+ description: "goodAtFirstThenBadProvider for sidecar",
+ mode: "sidecar",
+ interval: time.Duration(10) * time.Millisecond,
+ testTime: time.Duration(35) * time.Millisecond,
+ expectedCount: 1,
+ provider: goodAtFirstThenBadProvider,
+ assertOn: "fail",
+ },
+ }
+
+ for _, tc := range TestCases {
+ //var logBuffer bytes.Buffer
+ var err error
+ //logger.InfoLogger = log.New(&logBuffer, "", 0)
+
+ // Construct a secret provider function
+ var providerQuit = make(chan struct{})
+ provideSecrets := PeriodicSecretProvider(
+ tc.interval, tc.mode, tc.provider, providerQuit,
+ )
+
+ providerCount = 0
+ testError := make(chan error)
+ go func() {
+ err := provideSecrets()
+ testError <- err
+ }()
+ select {
+ case err = <-testError:
+ break
+ case <-time.After(tc.testTime):
+ providerQuit <- struct{}{}
+ //time.Sleep(5 * secretProviderGracePeriod)
+ break
+ }
+
+ if err == nil && providerCount != tc.expectedCount {
+ err = fmt.Errorf("%s: incorrect number of timer ticks, got %d expected %d",
+ tc.description, providerCount, tc.expectedCount)
+ }
+
+ if tc.assertOn == "fail" {
+ assert.NotNil(t, err)
+ assert.Contains(t, err.Error(), "Failed to Provide")
+ } else if tc.assertOn == "success" {
+ assert.NoError(t, err)
+ }
+ }
+}