From 2705f810d767a920f13679a23b49b453227d72fd Mon Sep 17 00:00:00 2001 From: John ODonnell Date: Thu, 16 Mar 2023 15:47:31 -0400 Subject: [PATCH] Convert cmd directory to unit testable entrypoint package --- cmd/secrets-provider/main.go | 260 +------------- pkg/entrypoint/entrypoint.go | 316 ++++++++++++++++++ pkg/entrypoint/entrypoint_test.go | 286 ++++++++++++++++ .../entrypoint}/trace.go | 8 +- .../conjur/conjur_secrets_retriever.go | 25 +- pkg/secrets/provide_conjur_secrets.go | 23 +- pkg/secrets/provider_status.go | 58 ++-- pkg/secrets/provider_status_test.go | 4 +- 8 files changed, 673 insertions(+), 307 deletions(-) create mode 100644 pkg/entrypoint/entrypoint.go create mode 100644 pkg/entrypoint/entrypoint_test.go rename {cmd/secrets-provider => pkg/entrypoint}/trace.go (89%) diff --git a/cmd/secrets-provider/main.go b/cmd/secrets-provider/main.go index 4a1c76d0..b29d138b 100644 --- a/cmd/secrets-provider/main.go +++ b/cmd/secrets-provider/main.go @@ -1,265 +1,9 @@ package main import ( - "context" - "errors" - "fmt" - "io/ioutil" - "os" - "time" - - authnConfigProvider "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" - "github.com/cyberark/conjur-authn-k8s-client/pkg/log" - "github.com/cyberark/conjur-opentelemetry-tracer/pkg/trace" - "github.com/cyberark/secrets-provider-for-k8s/pkg/log/messages" - "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets" - "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/annotations" - "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/clients/conjur" - secretsConfigProvider "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/config" - k8sSecretsStorage "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/k8s_secrets_storage" - "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/pushtofile" - "go.opentelemetry.io/otel/attribute" -) - -const ( - defaultContainerMode = "init" - annotationsFilePath = "/conjur/podinfo/annotations" - secretsBasePath = "/conjur/secrets" - templatesBasePath = "/conjur/templates" - tracerName = "secrets-provider" - tracerService = "secrets-provider" - tracerEnvironment = "production" - tracerID = 1 + "github.com/cyberark/secrets-provider-for-k8s/pkg/entrypoint" ) -var annotationsMap map[string]string - -var envAnnotationsConversion = map[string]string{ - "CONJUR_AUTHN_LOGIN": "conjur.org/authn-identity", - "CONTAINER_MODE": "conjur.org/container-mode", - "SECRETS_DESTINATION": "conjur.org/secrets-destination", - "K8S_SECRETS": "conjur.org/k8s-secrets", - "RETRY_COUNT_LIMIT": "conjur.org/retry-count-limit", - "RETRY_INTERVAL_SEC": "conjur.org/retry-interval-sec", - "DEBUG": "conjur.org/debug-logging", - "JAEGER_COLLECTOR_URL": "conjur.org/jaeger-collector-url", - "LOG_TRACES": "conjur.org/log-traces", - "JWT_TOKEN_PATH": "conjur.org/jwt-token-path", - "REMOVE_DELETED_SECRETS": "conjur.org/remove-deleted-secrets-enabled", -} - func main() { - // os.Exit() does not call deferred functions, so defer exit until after - // all other deferred functions have been called. - exitCode := 0 - defer func() { os.Exit(exitCode) }() - - logError := func(errStr string) { - log.Error(errStr) - exitCode = 1 - } - - log.Info(messages.CSPFK008I, secrets.FullVersionName) - - // Create a TracerProvider, Tracer, and top-level (parent) Span - tracerType, tracerURL := getTracerConfig() - ctx, tracer, deferFunc, err := createTracer(tracerType, tracerURL) - defer deferFunc(ctx) - if err != nil { - logError(err.Error()) - return - } - - // Process Pod Annotations - if err := processAnnotations(ctx, tracer); err != nil { - logError(err.Error()) - return - } - - // Gather K8s authenticator config and create a Conjur secret retriever - secretRetriever, err := secretRetriever(ctx, tracer) - if err != nil { - logError(err.Error()) - return - } - - // Gather secrets config and create a repeatable Secrets Provider - provideSecrets, _, err := repeatableSecretsProvider(ctx, tracer, secretRetriever) - if err != nil { - logError(err.Error()) - return - } - - // Provide secrets - if err = provideSecrets(); err != nil { - logError(err.Error()) - } -} - -func processAnnotations(ctx context.Context, tracer trace.Tracer) error { - // Only attempt to populate from annotations if the annotations file exists - // TODO: Figure out strategy for dealing with explicit annotation file path - // set by user. In that case we can't just ignore that the file is missing. - if _, err := os.Stat(annotationsFilePath); err == nil { - _, span := tracer.Start(ctx, "Process Annotations") - defer span.End() - annotationsMap, err = annotations.NewAnnotationsFromFile(annotationsFilePath) - if err != nil { - log.Error(err.Error()) - span.RecordErrorAndSetStatus(err) - return err - } - - errLogs, infoLogs := secretsConfigProvider.ValidateAnnotations(annotationsMap) - if err := logErrorsAndInfos(errLogs, infoLogs); err != nil { - log.Error(messages.CSPFK049E) - span.RecordErrorAndSetStatus(errors.New(messages.CSPFK049E)) - return err - } - } - return nil -} - -func secretRetriever(ctx context.Context, - tracer trace.Tracer) (*conjur.SecretRetriever, error) { - // Gather authenticator config - _, span := tracer.Start(ctx, "Gather authenticator config") - defer span.End() - - authnConfig, err := authnConfigProvider.NewConfigFromCustomEnv(ioutil.ReadFile, customEnv) - if err != nil { - span.RecordErrorAndSetStatus(err) - log.Error(messages.CSPFK008E) - return nil, err - } - - // Initialize a Conjur secret retriever - secretRetriever, err := conjur.NewSecretRetriever(authnConfig) - if err != nil { - log.Error(err.Error()) - return nil, err - } - return secretRetriever, nil -} - -func repeatableSecretsProvider( - ctx context.Context, - tracer trace.Tracer, - secretRetriever *conjur.SecretRetriever) (secrets.RepeatableProviderFunc, *secretsConfigProvider.Config, error) { - - _, span := tracer.Start(ctx, "Create repeatable secrets provider") - defer span.End() - - // Initialize Secrets Provider configuration - secretsConfig, err := setupSecretsConfig() - if err != nil { - log.Error(err.Error()) - span.RecordErrorAndSetStatus(err) - return nil, nil, err - } - providerConfig := &secrets.ProviderConfig{ - CommonProviderConfig: secrets.CommonProviderConfig{ - StoreType: secretsConfig.StoreType, - SanitizeEnabled: secretsConfig.SanitizeEnabled, - }, - K8sProviderConfig: k8sSecretsStorage.K8sProviderConfig{ - PodNamespace: secretsConfig.PodNamespace, - RequiredK8sSecrets: secretsConfig.RequiredK8sSecrets, - }, - P2FProviderConfig: pushtofile.P2FProviderConfig{ - SecretFileBasePath: secretsBasePath, - TemplateFileBasePath: templatesBasePath, - AnnotationsMap: annotationsMap, - }, - } - - // Tag the span with the secrets provider mode - span.SetAttributes(attribute.String("store_type", secretsConfig.StoreType)) - - // Create a secrets provider - provideSecrets, errs := secrets.NewProviderForType(ctx, - secretRetriever.Retrieve, *providerConfig) - if err := logErrorsAndInfos(errs, nil); err != nil { - log.Error(messages.CSPFK053E) - span.RecordErrorAndSetStatus(errors.New(messages.CSPFK053E)) - return nil, nil, err - } - - provideSecrets = secrets.RetryableSecretProvider( - time.Duration(secretsConfig.RetryIntervalSec)*time.Second, - 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{}) - - refreshConfig := secrets.ProviderRefreshConfig{ - Mode: getContainerMode(), - SecretRefreshInterval: secretsConfig.SecretsRefreshInterval, - ProviderQuit: providerQuit, - } - - repeatableProvideSecrets := secrets.RepeatableSecretProvider( - refreshConfig, - provideSecrets, - ) - return repeatableProvideSecrets, secretsConfig, nil -} - -func customEnv(key string) string { - if annotation, ok := envAnnotationsConversion[key]; ok { - if value := annotationsMap[annotation]; value != "" { - log.Info(messages.CSPFK014I, key, fmt.Sprintf("annotation %s", annotation)) - return value - } - - if value := os.Getenv(key); value == "" && key == "CONTAINER_MODE" { - log.Info(messages.CSPFK014I, key, "default") - return defaultContainerMode - } - - log.Info(messages.CSPFK014I, key, "environment") - } - - return os.Getenv(key) -} - -func setupSecretsConfig() (*secretsConfigProvider.Config, error) { - secretsProviderSettings := secretsConfigProvider.GatherSecretsProviderSettings(annotationsMap) - - errLogs, infoLogs := secretsConfigProvider.ValidateSecretsProviderSettings(secretsProviderSettings) - if err := logErrorsAndInfos(errLogs, infoLogs); err != nil { - log.Error(messages.CSPFK015E) - return nil, err - } - - return secretsConfigProvider.NewConfig(secretsProviderSettings), nil -} - -func logErrorsAndInfos(errLogs []error, infoLogs []error) error { - for _, err := range infoLogs { - log.Info(err.Error()) - } - if len(errLogs) > 0 { - for _, err := range errLogs { - log.Error(err.Error()) - } - return errors.New("fatal errors occurred, check Secrets Provider logs") - } - return nil -} - -func getContainerMode() string { - containerMode := "init" - if mode, exists := annotationsMap[secretsConfigProvider.ContainerModeKey]; exists { - containerMode = mode - } else if mode = os.Getenv("CONTAINER_MODE"); mode == "sidecar" || mode == "application" { - containerMode = mode - } - return containerMode + entrypoint.StartSecretsProvider() } diff --git a/pkg/entrypoint/entrypoint.go b/pkg/entrypoint/entrypoint.go new file mode 100644 index 00000000..066aee0b --- /dev/null +++ b/pkg/entrypoint/entrypoint.go @@ -0,0 +1,316 @@ +package entrypoint + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + "os" + "time" + + authnConfigProvider "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" + "github.com/cyberark/conjur-opentelemetry-tracer/pkg/trace" + "github.com/cyberark/secrets-provider-for-k8s/pkg/log/messages" + "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets" + "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/annotations" + "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/clients/conjur" + secretsConfigProvider "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/config" + k8sSecretsStorage "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/k8s_secrets_storage" + "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/pushtofile" + "go.opentelemetry.io/otel/attribute" +) + +const ( + defaultContainerMode = "init" + defaultAnnotationsFilePath = "/conjur/podinfo/annotations" + defaultSecretsBasePath = "/conjur/secrets" + defaultTemplatesBasePath = "/conjur/templates" + tracerName = "secrets-provider" + tracerService = "secrets-provider" + tracerEnvironment = "production" + tracerID = 1 +) + +var annotationsMap map[string]string + +var envAnnotationsConversion = map[string]string{ + "CONJUR_AUTHN_LOGIN": "conjur.org/authn-identity", + "CONTAINER_MODE": "conjur.org/container-mode", + "SECRETS_DESTINATION": "conjur.org/secrets-destination", + "K8S_SECRETS": "conjur.org/k8s-secrets", + "RETRY_COUNT_LIMIT": "conjur.org/retry-count-limit", + "RETRY_INTERVAL_SEC": "conjur.org/retry-interval-sec", + "DEBUG": "conjur.org/debug-logging", + "JAEGER_COLLECTOR_URL": "conjur.org/jaeger-collector-url", + "LOG_TRACES": "conjur.org/log-traces", + "JWT_TOKEN_PATH": "conjur.org/jwt-token-path", + "REMOVE_DELETED_SECRETS": "conjur.org/remove-deleted-secrets-enabled", +} + +func StartSecretsProvider() { + exitCode := StartSecretsProviderWithArguments( + defaultAnnotationsFilePath, + defaultSecretsBasePath, + defaultTemplatesBasePath, + conjur.NewSecretRetriever, + secrets.NewProviderForType, + secrets.DefaultStatusUpdater, + ) + os.Exit(exitCode) +} + +func StartSecretsProviderWithArguments( + annotationsFilePath string, + secretsBasePath string, + templatesBasePath string, + retrieverFactory conjur.RetrieverFactory, + providerFactory secrets.ProviderFactory, + statusUpdater secrets.StatusUpdater, +) (exitCode int) { + // os.Exit() does not call deferred functions, so defer exit until after + // all other deferred functions have been called. + exitCode = 0 + + logError := func(errStr string) { + log.Error(errStr) + exitCode = 1 + } + + log.Info(messages.CSPFK008I, secrets.FullVersionName) + + // Create a TracerProvider, Tracer, and top-level (parent) Span + tracerType, tracerURL := getTracerConfig(annotationsFilePath) + ctx, tracer, deferFunc, err := createTracer(tracerType, tracerURL) + defer deferFunc(ctx) + if err != nil { + logError(err.Error()) + return + } + + // Process Pod Annotations + if err := processAnnotations( + annotationsFilePath, + ctx, + tracer, + ); err != nil { + logError(err.Error()) + return + } + + // Gather K8s authenticator config and create a Conjur secret retriever + secretRetriever, err := secretRetriever( + ctx, + tracer, + retrieverFactory, + ) + if err != nil { + logError(err.Error()) + return + } + + // Gather secrets config and create a repeatable Secrets Provider + provideSecrets, _, err := repeatableSecretsProvider( + ctx, + tracer, + secretsBasePath, + templatesBasePath, + secretRetriever, + providerFactory, + statusUpdater, + ) + if err != nil { + logError(err.Error()) + return + } + + // Provide secrets + if err = provideSecrets(); err != nil { + logError(err.Error()) + } + return +} + +func processAnnotations( + annotationsFilePath string, + ctx context.Context, + tracer trace.Tracer, +) error { + // Only attempt to populate from annotations if the annotations file exists + // TODO: Figure out strategy for dealing with explicit annotation file path + // set by user. In that case we can't just ignore that the file is missing. + if _, err := os.Stat(annotationsFilePath); err == nil { + _, span := tracer.Start(ctx, "Process Annotations") + defer span.End() + annotationsMap, err = annotations.NewAnnotationsFromFile(annotationsFilePath) + if err != nil { + log.Error(err.Error()) + span.RecordErrorAndSetStatus(err) + return err + } + + errLogs, infoLogs := secretsConfigProvider.ValidateAnnotations(annotationsMap) + if err := logErrorsAndInfos(errLogs, infoLogs); err != nil { + log.Error(messages.CSPFK049E) + span.RecordErrorAndSetStatus(errors.New(messages.CSPFK049E)) + return err + } + } + return nil +} + +func secretRetriever( + ctx context.Context, + tracer trace.Tracer, + retrieverFactory conjur.RetrieverFactory, +) (conjur.SecretRetriever, error) { + // Gather authenticator config + _, span := tracer.Start(ctx, "Gather authenticator config") + defer span.End() + + authnConfig, err := authnConfigProvider.NewConfigFromCustomEnv(ioutil.ReadFile, customEnv) + if err != nil { + span.RecordErrorAndSetStatus(err) + log.Error(messages.CSPFK008E) + return nil, err + } + + // Initialize a Conjur secret retriever + secretRetriever, err := retrieverFactory(authnConfig) + if err != nil { + log.Error(err.Error()) + return nil, err + } + return secretRetriever, nil +} + +func repeatableSecretsProvider( + ctx context.Context, + tracer trace.Tracer, + secretsBasePath string, + templatesBasePath string, + secretRetriever conjur.SecretRetriever, + providerFactory secrets.ProviderFactory, + statusUpdater secrets.StatusUpdater, +) (secrets.RepeatableProviderFunc, *secretsConfigProvider.Config, error) { + + _, span := tracer.Start(ctx, "Create repeatable secrets provider") + defer span.End() + + // Initialize Secrets Provider configuration + secretsConfig, err := setupSecretsConfig() + if err != nil { + log.Error(err.Error()) + span.RecordErrorAndSetStatus(err) + return nil, nil, err + } + providerConfig := &secrets.ProviderConfig{ + CommonProviderConfig: secrets.CommonProviderConfig{ + StoreType: secretsConfig.StoreType, + SanitizeEnabled: secretsConfig.SanitizeEnabled, + }, + K8sProviderConfig: k8sSecretsStorage.K8sProviderConfig{ + PodNamespace: secretsConfig.PodNamespace, + RequiredK8sSecrets: secretsConfig.RequiredK8sSecrets, + }, + P2FProviderConfig: pushtofile.P2FProviderConfig{ + SecretFileBasePath: secretsBasePath, + TemplateFileBasePath: templatesBasePath, + AnnotationsMap: annotationsMap, + }, + } + + // Tag the span with the secrets provider mode + span.SetAttributes(attribute.String("store_type", secretsConfig.StoreType)) + + // Create a secrets provider + provideSecrets, errs := providerFactory( + ctx, + secretRetriever.Retrieve, + *providerConfig, + ) + if err := logErrorsAndInfos(errs, nil); err != nil { + log.Error(messages.CSPFK053E) + span.RecordErrorAndSetStatus(errors.New(messages.CSPFK053E)) + return nil, nil, err + } + + provideSecrets = secrets.RetryableSecretProvider( + time.Duration(secretsConfig.RetryIntervalSec)*time.Second, + 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{}) + + refreshConfig := secrets.ProviderRefreshConfig{ + Mode: getContainerMode(), + SecretRefreshInterval: secretsConfig.SecretsRefreshInterval, + ProviderQuit: providerQuit, + } + + repeatableProvideSecrets := secrets.RepeatableSecretProvider( + refreshConfig, + provideSecrets, + statusUpdater, + ) + return repeatableProvideSecrets, secretsConfig, nil +} + +func customEnv(key string) string { + if annotation, ok := envAnnotationsConversion[key]; ok { + if value := annotationsMap[annotation]; value != "" { + log.Info(messages.CSPFK014I, key, fmt.Sprintf("annotation %s", annotation)) + return value + } + + if value := os.Getenv(key); value == "" && key == "CONTAINER_MODE" { + log.Info(messages.CSPFK014I, key, "default") + return defaultContainerMode + } + + log.Info(messages.CSPFK014I, key, "environment") + } + + return os.Getenv(key) +} + +func setupSecretsConfig() (*secretsConfigProvider.Config, error) { + secretsProviderSettings := secretsConfigProvider.GatherSecretsProviderSettings(annotationsMap) + + errLogs, infoLogs := secretsConfigProvider.ValidateSecretsProviderSettings(secretsProviderSettings) + if err := logErrorsAndInfos(errLogs, infoLogs); err != nil { + log.Error(messages.CSPFK015E) + return nil, err + } + + return secretsConfigProvider.NewConfig(secretsProviderSettings), nil +} + +func logErrorsAndInfos(errLogs []error, infoLogs []error) error { + for _, err := range infoLogs { + log.Info(err.Error()) + } + if len(errLogs) > 0 { + for _, err := range errLogs { + log.Error(err.Error()) + } + return errors.New("fatal errors occurred, check Secrets Provider logs") + } + return nil +} + +func getContainerMode() string { + containerMode := "init" + if mode, exists := annotationsMap[secretsConfigProvider.ContainerModeKey]; exists { + containerMode = mode + } else if mode = os.Getenv("CONTAINER_MODE"); mode == "sidecar" || mode == "application" { + containerMode = mode + } + return containerMode +} diff --git a/pkg/entrypoint/entrypoint_test.go b/pkg/entrypoint/entrypoint_test.go new file mode 100644 index 00000000..f1c9f908 --- /dev/null +++ b/pkg/entrypoint/entrypoint_test.go @@ -0,0 +1,286 @@ +package entrypoint + +import ( + "bytes" + "context" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" + "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets" + "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/clients/conjur" + "github.com/stretchr/testify/assert" +) + +type mockRetrieverFactory struct { + retriever conjur.SecretRetriever + err error +} + +func (r mockRetrieverFactory) GetRetriever(a config.Configuration) (conjur.SecretRetriever, error) { + return r.retriever, r.err +} + +type mockRetriever struct { + data map[string][]byte + err error +} + +func (r mockRetriever) Retrieve(ids []string, c context.Context) (map[string][]byte, error) { + return r.data, r.err +} + +type mockProviderFactory struct { + providerFunc secrets.ProviderFunc + errs []error +} + +func (p mockProviderFactory) GetProvider(traceContext context.Context, secretsRetrieverFunc conjur.RetrieveSecretsFunc, providerConfig secrets.ProviderConfig) (secrets.ProviderFunc, []error) { + return p.providerFunc, p.errs +} + +type mockStatusUpdater struct{} + +func (s mockStatusUpdater) SetSecretsProvided() error { + return nil +} + +func (s mockStatusUpdater) SetSecretsUpdated() error { + return nil +} + +func (s mockStatusUpdater) CopyScripts() error { + return nil +} + +type editableMap map[string]string + +func (m editableMap) Delete(key string) editableMap { + delete(m, key) + return m +} + +func (m editableMap) Edit(key string, value string) editableMap { + m[key] = value + return m +} + +func (m editableMap) Copy() editableMap { + mCopy := editableMap{} + for k, v := range m { + mCopy[k] = v + } + return mCopy +} + +func TestStartSecretsProvider(t *testing.T) { + tmpDir, _ := ioutil.TempDir("", "entrypoint_test") + defer os.RemoveAll(tmpDir) + + env := map[string]string{ + "MY_POD_NAME": "podname", + "MY_POD_NAMESPACE": "podnamespace", + "CONJUR_ACCOUNT": "default", + "CONJUR_APPLIANCE_URL": "https://conjur.myorg.com", + "CONJUR_AUTHN_URL": "https://conjur.myorg.com/authn-k8s/authn-service", + "CONJUR_SSL_CERTIFICATE": "cert-data", + "CONJUR_AUTHENTICATOR_ID": "authn-service", + } + annots := editableMap{ + "conjur.org/authn-identity": "host/alice", + "conjur.org/container-mode": "init", + "conjur.org/secrets-destination": "file", + "conjur.org/conjur-secrets.groupA": `- alias: path/to/secret\n`, + } + annotationsFilePath := filepath.Join(tmpDir, "annotations") + secretsBasePath := filepath.Join(tmpDir, "secrets") + templatesBasePath := filepath.Join(tmpDir, "templates") + + TestCases := []struct { + description string + environment map[string]string + annotations map[string]string + retrieverFactory mockRetrieverFactory + providerFactory mockProviderFactory + assertions func(*testing.T, int, string) + }{ + { + description: "happy path", + environment: env, + annotations: annots, + retrieverFactory: mockRetrieverFactory{ + retriever: mockRetriever{ + data: map[string][]byte{ + "path/to/secret": []byte("secret value"), + }, + err: nil, + }, + }, + providerFactory: mockProviderFactory{ + providerFunc: func() (bool, error) { + return true, nil + }, + errs: []error{}, + }, + assertions: func(t *testing.T, code int, logs string) { + assert.Equal(t, 0, code) + }, + }, + { + description: "bad provider factory", + environment: env, + annotations: annots, + retrieverFactory: mockRetrieverFactory{ + retriever: mockRetriever{ + data: map[string][]byte{ + "path/to/secret": []byte("secret value"), + }, + err: nil, + }, + }, + providerFactory: mockProviderFactory{ + providerFunc: func() (bool, error) { + return true, nil + }, + errs: []error{errors.New("provider factory failure")}, + }, + assertions: func(t *testing.T, code int, logs string) { + assert.Equal(t, 1, code) + assert.Contains(t, logs, "CSPFK053E") + assert.Contains(t, logs, "provider factory failure") + }, + }, + { + description: "bad retriever factory", + environment: env, + annotations: annots, + retrieverFactory: mockRetrieverFactory{ + retriever: mockRetriever{ + data: nil, + err: nil, + }, + err: errors.New("retriever factory failure"), + }, + providerFactory: mockProviderFactory{ + providerFunc: func() (bool, error) { + return true, nil + }, + errs: []error{}, + }, + assertions: func(t *testing.T, code int, logs string) { + assert.Equal(t, 1, code) + assert.Contains(t, logs, "retriever factory failure") + }, + }, + { + description: "annotation validation failure", + environment: env, + annotations: annots.Copy().Edit("conjur.org/retry-interval-sec", "not-an-integer"), + retrieverFactory: mockRetrieverFactory{ + retriever: mockRetriever{ + data: nil, + err: nil, + }, + err: nil, + }, + providerFactory: mockProviderFactory{ + providerFunc: func() (bool, error) { + return true, nil + }, + errs: []error{}, + }, + assertions: func(t *testing.T, code int, logs string) { + assert.Equal(t, 1, code) + assert.Contains(t, logs, "CSPFK049E") + }, + }, + { + description: "authenticator config validation failure", + environment: env, + annotations: annots.Copy().Edit("conjur.org/authn-identity", "1"), + retrieverFactory: mockRetrieverFactory{ + retriever: mockRetriever{ + data: nil, + err: nil, + }, + err: nil, + }, + providerFactory: mockProviderFactory{ + providerFunc: func() (bool, error) { + return true, nil + }, + errs: []error{}, + }, + assertions: func(t *testing.T, code int, logs string) { + assert.Equal(t, 1, code) + assert.Contains(t, logs, "CSPFK008E") + }, + }, + { + description: "secrets provider setting validation failure", + environment: env, + annotations: annots.Copy().Delete("conjur.org/secrets-destination"), + retrieverFactory: mockRetrieverFactory{ + retriever: mockRetriever{ + data: nil, + err: nil, + }, + err: nil, + }, + providerFactory: mockProviderFactory{ + providerFunc: func() (bool, error) { + return true, nil + }, + errs: []error{}, + }, + assertions: func(t *testing.T, code int, logs string) { + assert.Equal(t, 1, code) + assert.Contains(t, logs, "CSPFK015E") + }, + }, + } + + for _, tc := range TestCases { + t.Run(tc.description, func(t *testing.T) { + // Capture error logs + buf := &bytes.Buffer{} + log.ErrorLogger.SetOutput(buf) + // Burn info logs + log.InfoLogger.SetOutput(&bytes.Buffer{}) + // Setup envvars + for k, v := range tc.environment { + os.Setenv(k, v) + } + // Setup annotation file + annotationFileContent := "" + for k, v := range tc.annotations { + annotationFileContent = fmt.Sprintf("%s%s=\"%s\"\n", annotationFileContent, k, v) + } + err := os.WriteFile(annotationsFilePath, []byte(annotationFileContent), 0666) + assert.Nil(t, err) + + exitCode := StartSecretsProviderWithArguments( + annotationsFilePath, + secretsBasePath, + templatesBasePath, + tc.retrieverFactory.GetRetriever, + tc.providerFactory.GetProvider, + mockStatusUpdater{}, + ) + + tc.assertions(t, exitCode, buf.String()) + + // Restore logs + log.ErrorLogger.SetOutput(os.Stderr) + // Teardown envvars + for k := range tc.environment { + os.Setenv(k, "") + } + }) + } +} diff --git a/cmd/secrets-provider/trace.go b/pkg/entrypoint/trace.go similarity index 89% rename from cmd/secrets-provider/trace.go rename to pkg/entrypoint/trace.go index 89acdcf4..f74396cb 100644 --- a/cmd/secrets-provider/trace.go +++ b/pkg/entrypoint/trace.go @@ -1,4 +1,4 @@ -package main +package entrypoint import ( "context" @@ -9,10 +9,10 @@ import ( "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/annotations" ) -func getTracerConfig() (trace.TracerProviderType, string) { +func getTracerConfig(annotationsFilePath string) (trace.TracerProviderType, string) { // First try to get the tracer config from annotations log.Debug("Getting tracer config from annotations") - traceType, jaegerUrl, err := getTracerConfigFromAnnotations() + traceType, jaegerUrl, err := getTracerConfigFromAnnotations(annotationsFilePath) // If no tracer is specified in annotations, get it from environment variables if err != nil || traceType == trace.NoopProviderType { @@ -35,7 +35,7 @@ func getTracerConfigFromEnv() (trace.TracerProviderType, string) { return trace.NoopProviderType, "" } -func getTracerConfigFromAnnotations() (trace.TracerProviderType, string, error) { +func getTracerConfigFromAnnotations(annotationsFilePath string) (trace.TracerProviderType, string, error) { annotationsMap, err := annotations.NewAnnotationsFromFile(annotationsFilePath) if err != nil { return trace.NoopProviderType, "", err diff --git a/pkg/secrets/clients/conjur/conjur_secrets_retriever.go b/pkg/secrets/clients/conjur/conjur_secrets_retriever.go index 3ffeca3a..1463b7da 100644 --- a/pkg/secrets/clients/conjur/conjur_secrets_retriever.go +++ b/pkg/secrets/clients/conjur/conjur_secrets_retriever.go @@ -16,19 +16,30 @@ import ( "github.com/cyberark/secrets-provider-for-k8s/pkg/log/messages" ) -// SecretRetriever implements a Retrieve function that is capable of -// authenticating with Conjur and retrieving multiple Conjur variables -// in bulk. -type SecretRetriever struct { +// SecretRetriever defines a interface for an ambiguous secret retriever, with +// a Retrieve function that returns a map of variable IDs to byte slices of +// secret data given an array of variable IDs. +type SecretRetriever interface { + Retrieve([]string, context.Context) (map[string][]byte, error) +} + +// SecretRetrieverConfig implements SecretRetriever, with a Retrieve function +// that is capable of authenticating with Conjur and retrieving multiple Conjur +// variables in bulk. +type SecretRetrieverConfig struct { authn authenticator.Authenticator } +// RetrieverFactory defines a function type for creating a SecretRetriever +// implementation given an authenticator config. +type RetrieverFactory func(authnConfig config.Configuration) (SecretRetriever, error) + // RetrieveSecretsFunc defines a function type for retrieving secrets. type RetrieveSecretsFunc func(variableIDs []string, traceContext context.Context) (map[string][]byte, error) // NewSecretRetriever creates a new SecretRetriever and Authenticator // given an authenticator config. -func NewSecretRetriever(authnConfig config.Configuration) (*SecretRetriever, error) { +func NewSecretRetriever(authnConfig config.Configuration) (SecretRetriever, error) { accessToken, err := memory.NewAccessToken() if err != nil { return nil, fmt.Errorf("%s", messages.CSPFK001E) @@ -39,14 +50,14 @@ func NewSecretRetriever(authnConfig config.Configuration) (*SecretRetriever, err return nil, fmt.Errorf("%s", messages.CSPFK009E) } - return &SecretRetriever{ + return &SecretRetrieverConfig{ authn: authn, }, nil } // Retrieve implements a RetrieveSecretsFunc for a given SecretRetriever. // Authenticates the client, and retrieves a given batch of variables from Conjur. -func (retriever SecretRetriever) Retrieve(variableIDs []string, traceContext context.Context) (map[string][]byte, error) { +func (retriever SecretRetrieverConfig) Retrieve(variableIDs []string, traceContext context.Context) (map[string][]byte, error) { authn := retriever.authn diff --git a/pkg/secrets/provide_conjur_secrets.go b/pkg/secrets/provide_conjur_secrets.go index d2891ba7..8722728d 100644 --- a/pkg/secrets/provide_conjur_secrets.go +++ b/pkg/secrets/provide_conjur_secrets.go @@ -34,6 +34,10 @@ type ProviderConfig struct { pushtofile.P2FProviderConfig } +// ProviderFactory defines a function type for creating a ProviderFunc given a +// RetrieveSecretsFunc and ProviderConfig. +type ProviderFactory func(traceContent context.Context, secretsRetrieverFunc conjur.RetrieveSecretsFunc, providerConfig ProviderConfig) (ProviderFunc, []error) + // ProviderFunc describes a function type responsible for providing secrets to // an unspecified target. It returns either an error, or a flag that indicates // whether any target secret files or Kubernetes Secrets have been updated. @@ -118,21 +122,22 @@ type ProviderRefreshConfig struct { // RepeatableSecretProvider 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) +// - 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 RepeatableSecretProvider( refreshConfig ProviderRefreshConfig, provideSecrets ProviderFunc, + statusUpdater StatusUpdater, ) RepeatableProviderFunc { return repeatableSecretProvider(refreshConfig, provideSecrets, - defaultStatusUpdater) + statusUpdater) } func repeatableSecretProvider( config ProviderRefreshConfig, provideSecrets ProviderFunc, - status statusUpdater, + status StatusUpdater, ) RepeatableProviderFunc { var periodicQuit = make(chan struct{}) @@ -141,14 +146,14 @@ func repeatableSecretProvider( var err error return func() error { - if err = status.copyScripts(); err != nil { + if err = status.CopyScripts(); err != nil { return err } if _, err = provideSecrets(); err != nil { // Return immediately upon error, regardless of operating mode return err } - err = status.setSecretsProvided() + err = status.SetSecretsProvided() if err != nil { return err } @@ -200,7 +205,7 @@ type periodicConfig struct { func periodicSecretProvider( provideSecrets ProviderFunc, config periodicConfig, - status statusUpdater, + status StatusUpdater, ) { for { select { @@ -209,7 +214,7 @@ func periodicSecretProvider( case <-config.ticker.C: updated, err := provideSecrets() if err == nil && updated { - err = status.setSecretsUpdated() + err = status.SetSecretsUpdated() } if err != nil { config.periodicError <- err diff --git a/pkg/secrets/provider_status.go b/pkg/secrets/provider_status.go index 5505e408..6596671c 100644 --- a/pkg/secrets/provider_status.go +++ b/pkg/secrets/provider_status.go @@ -12,36 +12,40 @@ const ( scriptFileMode = 0755 ) -// statusUpdater defines an interface for recording a secret provider's +// StatusUpdater defines an interface for recording a secret provider's // status, and for copying utility scripts for checking that recorded status. // -// setSecretsProvided: A function that records that the secrets provider -// has finished providing secrets (at least for its -// initial iteration). -// setSecretsUpdated: A function that records that the secrets provider -// has just updated the secret files or Kubernetes Secrets -// with recently updated secret values retrieved from -// Conjur. -// copyScripts: Copy utility scripts for checking provider status from -// a "baked-in" container directory into a volume that is -// potentially shared with application container(s). -type statusUpdater interface { - setSecretsProvided() error - setSecretsUpdated() error - copyScripts() error +// SetSecretsProvided: A function that records that the secrets provider +// +// has finished providing secrets (at least for its +// initial iteration). +// +// SetSecretsUpdated: A function that records that the secrets provider +// +// has just updated the secret files or Kubernetes Secrets +// with recently updated secret values retrieved from +// Conjur. +// +// CopyScripts: Copy utility scripts for checking provider status from +// +// a "baked-in" container directory into a volume that is +// potentially shared with application container(s). +type StatusUpdater interface { + SetSecretsProvided() error + SetSecretsUpdated() error + CopyScripts() error } -type chmodFunc func(string, os.FileMode) error -type createFunc func(string) (*os.File, error) -type openFunc func(string) (*os.File, error) +type chmodFunc func(string, os.FileMode) error +type createFunc func(string) (*os.File, error) +type openFunc func(string) (*os.File, error) type mkdirAllFunc func(string, os.FileMode) error - type osFuncs struct { - chmod chmodFunc - create createFunc - open openFunc - mkdirAll mkdirAllFunc + chmod chmodFunc + create createFunc + open openFunc + mkdirAll mkdirAllFunc } var stdOSFuncs = osFuncs{ @@ -62,7 +66,7 @@ type fileUpdater struct { os osFuncs } -var defaultStatusUpdater = fileUpdater{ +var DefaultStatusUpdater = fileUpdater{ providedFile: "/conjur/status/CONJUR_SECRETS_PROVIDED", updatedFile: "/conjur/status/CONJUR_SECRETS_UPDATED", scripts: []string{"conjur-secrets-unchanged.sh"}, @@ -80,15 +84,15 @@ func (f fileUpdater) setStatus(path string) error { return f.os.chmod(file.Name(), statusFileMode) } -func (f fileUpdater) setSecretsProvided() error { +func (f fileUpdater) SetSecretsProvided() error { return f.setStatus(f.providedFile) } -func (f fileUpdater) setSecretsUpdated() error { +func (f fileUpdater) SetSecretsUpdated() error { return f.setStatus(f.updatedFile) } -func (f fileUpdater) copyScripts() error { +func (f fileUpdater) CopyScripts() error { // Create the directory err := f.os.mkdirAll(f.scriptDestDir, os.ModePerm) diff --git a/pkg/secrets/provider_status_test.go b/pkg/secrets/provider_status_test.go index 84a424d7..dfa5d05c 100644 --- a/pkg/secrets/provider_status_test.go +++ b/pkg/secrets/provider_status_test.go @@ -239,10 +239,10 @@ func TestCopyScripts(t *testing.T) { // Run test fileUpdater := updater.fileUpdater - err = fileUpdater.copyScripts() + err = fileUpdater.CopyScripts() if tc.runTwice { assert.NoError(t, err) - err = fileUpdater.copyScripts() + err = fileUpdater.CopyScripts() } // Check results