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 Annotation parsing and Config from Annotations functions #359

Merged
merged 10 commits into from
Sep 27, 2021
46 changes: 38 additions & 8 deletions cmd/secrets-provider/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,44 @@ import (

"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"
secretsConfigProvider "github.com/cyberark/secrets-provider-for-k8s/pkg/secrets/config"
"github.com/cyberark/secrets-provider-for-k8s/pkg/utils"
)

const annotationsFile = "/conjur/podinfo/annotations"

func main() {
var err error

log.Info(messages.CSPFK008I, secrets.FullVersionName)

// Initialize configurations
// Initialize authn configuration
authnConfig, err := authnConfigProvider.NewFromEnv()
if err != nil {
printErrorAndExit(messages.CSPFK008E)
}

secretsConfig, err := secretsConfigProvider.NewFromEnv()
if err != nil {
printErrorAndExit(messages.CSPFK015E)
validateContainerMode(authnConfig.ContainerMode)

annotationsMap := map[string]string{}
if _, err := os.Stat(annotationsFile); err == nil {
annotationsMap, err = annotations.FromFile(annotationsFile)
if err != nil {
printErrorAndExit(messages.CSPFK040E)
}
}

validateContainerMode(authnConfig.ContainerMode)
errLogs, infoLogs := secretsConfigProvider.ValidateAnnotations(annotationsMap)
logErrorsAndConditionalExit(errLogs, infoLogs, messages.CSPFK049E)

secretsProviderSettings := secretsConfigProvider.GatherSecretsProviderSettings(annotationsMap)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on fitting together all of the config puzzle pieces here.


errLogs, infoLogs = secretsConfigProvider.ValidateSecretsProviderSettings(secretsProviderSettings)
logErrorsAndConditionalExit(errLogs, infoLogs, messages.CSPFK015E)

// Initialize Secrets Provider configuration
secretsConfig := secretsConfigProvider.NewConfig(secretsProviderSettings)

provideConjurSecrets, err := secrets.GetProvideConjurSecretFunc(secretsConfig.StoreType)
if err != nil {
Expand All @@ -59,7 +76,7 @@ func main() {
log.Info(fmt.Sprintf(messages.CSPFK010I, limitedBackOff.RetryCount(), limitedBackOff.RetryLimit))
}

return provideSecretsToTarget(authn, provideConjurSecrets, accessToken)
return provideSecretsToTarget(authn, provideConjurSecrets, accessToken, secretsConfig)
}, limitedBackOff)

if err != nil {
Expand All @@ -75,14 +92,15 @@ func main() {
}
}

func provideSecretsToTarget(authn *authenticator.Authenticator, provideConjurSecrets secrets.ProvideConjurSecrets, accessToken *memory.AccessToken) error {
func provideSecretsToTarget(authn *authenticator.Authenticator, provideConjurSecrets secrets.ProvideConjurSecrets,
accessToken *memory.AccessToken, secretsConfig *secretsConfigProvider.Config) error {
log.Info(fmt.Sprintf(messages.CSPFK001I, authn.Config.Username))
err := authn.Authenticate()
if err != nil {
return log.RecordedError(messages.CSPFK010E)
}

err = provideConjurSecrets(accessToken)
err = provideConjurSecrets(accessToken, secretsConfig)
if err != nil {
return log.RecordedError(messages.CSPFK016E)
}
Expand All @@ -101,6 +119,18 @@ func printErrorAndExit(errorMessage string) {
os.Exit(1)
}

func logErrorsAndConditionalExit(errLogs []error, infoLogs []error, failureMsg string) {
for _, err := range infoLogs {
log.Info(err.Error())
}
if len(errLogs) > 0 {
for _, err := range errLogs {
log.Error(err.Error())
}
printErrorAndExit(failureMsg)
}
}

func validateContainerMode(containerMode string) {
validContainerModes := []string{
"init",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ deploy_init_env

pod_name="$(get_pod_name "$APP_NAMESPACE_NAME" 'app=test-env')"

echo "Expecting secrets provider to fail with error 'CSPFK004E Environment variable 'SECRETS_DESTINATION' must be provided'"
$cli_with_timeout "logs $pod_name -c cyberark-secrets-provider-for-k8s | grep CSPFK004E"
echo "Expecting secrets provider to fail with error 'CSPFK046E Secret Store Type needs to be configured, either with 'SECRETS_DESTINATION' environment variable or 'conjur.org/secrets-destination' Pod annotation'"
$cli_with_timeout "logs $pod_name -c cyberark-secrets-provider-for-k8s | grep CSPFK046E"
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ deploy_init_env
echo "Expecting for 'CrashLoopBackOff' state of pod test-env"
wait_for_it 600 "get_pods_info | grep CrashLoopBackOff"

echo "Expecting secrets provider to fail with error 'CSPFK004E Environment variable K8S_SECRETS must be provided'"
echo "Expecting secrets provider to fail with error 'CSPFK048E Secrets Provider in K8s Secrets mode requires either the 'K8S_SECRETS' environment variable or 'conjur.org/k8s-secrets' Pod annotation'"
pod_name="$(get_pod_name "$APP_NAMESPACE_NAME" 'app=test-env')"

$cli_with_timeout "logs $pod_name -c cyberark-secrets-provider-for-k8s | grep CSPFK004E"
$cli_with_timeout "logs $pod_name -c cyberark-secrets-provider-for-k8s | grep CSPFK048E"
4 changes: 2 additions & 2 deletions deploy/test/test_cases/TEST_ID_8_K8S_SECRETS_env_var_empty.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ deploy_init_env
echo "Expecting for CrashLoopBackOff state of pod test-env"
wait_for_it 600 "get_pods_info | grep CrashLoopBackOff"

echo "Expecting Secrets provider to fail with error 'CSPFK004E Environment variable K8S_SECRETS must be provided'"
echo "Expecting secrets provider to fail with error 'CSPFK048E Secrets Provider in K8s Secrets mode requires either 'K8S_SECRETS' environment variable or 'conjur.org/k8s-secrets' Pod annotation'"
pod_name="$(get_pod_name "$APP_NAMESPACE_NAME" 'app=test-env')"

$cli_with_timeout "logs $pod_name -c cyberark-secrets-provider-for-k8s | grep CSPFK004E"
$cli_with_timeout "logs $pod_name -c cyberark-secrets-provider-for-k8s | grep CSPFK048E"
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/onsi/ginkgo v1.14.0 // indirect
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.3.0
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f // indirect
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
13 changes: 13 additions & 0 deletions pkg/log/messages/error_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,16 @@ const CSPFK037E string = "CSPFK037E Failed to parse DAP/Conjur variable IDs"
// General
const CSPFK038E string = "CSPFK038E Retransmission backoff exhausted"
const CSPFK039E string = "CSPFK039E Secrets Provider for Kubernetes failed to update Kubernetes Secrets"

// Annotations
const CSPFK040E string = "CSPFK040E Failed to parse annotations file"
const CSPFK041E string = "CSPFK041E Failed to open annotations file '%s'. Reason: %s"
const CSPFK042E string = "CSPFK042E Annotation '%s' does not accept value '%s': must be type %s"
const CSPFK043E string = "CSPFK043E Annotation '%s' does not accept value '%s': only accepts %v"
const CSPFK044E string = "CSPFK044E Annotation '%s' must be provided"
const CSPFK045E string = "CSPFK045E Annotation file line %d is malformed: expecting format \"<key>=<quoted value>\""

const CSPFK046E string = "CSPFK046E Secret Store Type needs to be configured, either with 'SECRETS_DESTINATION' environment variable or 'conjur.org/secrets-destination' Pod annotation"
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"
2 changes: 2 additions & 0 deletions pkg/log/messages/info_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ const CSPFK007I string = "CSPFK007I Attempting to re-authenticate: %d retries ou
const CSPFK008I string = "CSPFK008I CyberArk Secrets Provider for Kubernetes v%s starting up"
const CSPFK009I string = "CSPFK009I DAP/Conjur Secrets updated in Kubernetes successfully"
const CSPFK010I string = "CSPFK010I Updating Kubernetes Secrets: %d retries out of %d"
const CSPFK011I string = "CSPFK011I Annotation '%s' valid, but not recognized"
const CSPFK012I string = "CSPFK012I Secrets Provider setting '%s' set by both environment variable '%s' and annotation '%s'"
63 changes: 63 additions & 0 deletions pkg/secrets/annotations/annotation_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package annotations

import (
"bufio"
"io"
"os"
"strconv"
"strings"

"github.com/cyberark/conjur-authn-k8s-client/pkg/log"
"github.com/cyberark/secrets-provider-for-k8s/pkg/log/messages"
)

// FromFile reads and parses and annotations file that has been created
// by Kubernetes via the Downward API, based on Pod annotations that are defined
// in a deployment manifest.
func FromFile(path string) (map[string]string, error) {
annotationsFile, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return nil, log.RecordedError(messages.CSPFK041E, path, err.Error())
}
defer annotationsFile.Close()
return ParseReader(annotationsFile)
}

// ParseReader parses an input stream representing an annotations file that
// had been created by Kubernetes via the Downward API, returning a string-to-string
// map of annotations key-value pairs.
//
// List and multi-line annotations are formatted as a single string in the annotations file,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment really helps!

// and this format persists into the map returned by this function.
// For example, the following annotation:
// conjur.org/conjur-secrets.cache: |
// - url
// - admin-password: password
// - admin-username: username
// Is stored in the annotations file as:
// conjur.org/conjur-secrets.cache="- url\n- admin-password: password\n- admin-username: username\n"
func ParseReader(annotationsFile io.Reader) (map[string]string, error) {
var lines []string
scanner := bufio.NewScanner(annotationsFile)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}

annotationsMap := make(map[string]string)
for lineNumber, line := range lines {
keyValuePair := strings.SplitN(line, "=", 2)
if len(keyValuePair) == 1 {
return nil, log.RecordedError(messages.CSPFK045E, lineNumber+1)
}

key := keyValuePair[0]
value, err := strconv.Unquote(keyValuePair[1])
if err != nil {
return nil, log.RecordedError(messages.CSPFK045E, lineNumber+1)
}

annotationsMap[key] = value
}

return annotationsMap, nil
}
98 changes: 98 additions & 0 deletions pkg/secrets/annotations/annotation_parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package annotations

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

type assertFunc func(t *testing.T, result map[string]string, err error)

type parseReaderTestCase struct {
description string
contents string
assert assertFunc
}

func assertGoodAnnotations(expected map[string]string) assertFunc {
return func(t *testing.T, result map[string]string, err error) {
if !assert.NoError(t, err) {
return
}

assert.Equal(t, expected, result)
}
}

func assertEmptyMap() assertFunc {
return func(t *testing.T, result map[string]string, err error) {
if !assert.NoError(t, err) {
return
}

assert.Equal(t, map[string]string{}, result)
}
}

func assertProperError(expectedErr string) assertFunc {
return func(t *testing.T, result map[string]string, err error) {
assert.Nil(t, result)
assert.Contains(t, err.Error(), expectedErr)
}
}

var parseReaderTestCases = []parseReaderTestCase{
{
description: "valid file",
contents: `conjur.org/authn-identity="host/conjur/authn-k8s/cluster/apps/inventory-api"
conjur.org/container-mode="init"
conjur.org/secrets-destination="k8s_secrets"
conjur.org/k8s-secrets="- k8s-secret-1\n- k8s-secret-2\n"
conjur.org/retry-count-limit="10"
conjur.org/retry-interval-sec="5"
conjur.org/debug-logging="true"
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"`,
assert: assertGoodAnnotations(
map[string]string{
"conjur.org/authn-identity": "host/conjur/authn-k8s/cluster/apps/inventory-api",
"conjur.org/container-mode": "init",
"conjur.org/secrets-destination": "k8s_secrets",
"conjur.org/k8s-secrets": "- k8s-secret-1\n- k8s-secret-2\n",
"conjur.org/retry-count-limit": "10",
"conjur.org/retry-interval-sec": "5",
"conjur.org/debug-logging": "true",
"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",
},
),
},
{
description: "an empty annotations file results in an empty map",
contents: "",
assert: assertEmptyMap(),
},
{
description: "malformed annotation file line with unquoted value",
contents: "conjur.org/container-mode=application",
assert: assertProperError("Annotation file line 1 is malformed"),
},
{
description: "malformed annotation file line without '='",
contents: `conjur.org/container-mode="application"
conjur.org/retry-count-limit: 5`,
assert: assertProperError("Annotation file line 2 is malformed"),
},
}

func TestParseReader(t *testing.T) {
for _, tc := range parseReaderTestCases {
t.Run(tc.description, func(t *testing.T) {
annotations, err := ParseReader(strings.NewReader(tc.contents))
tc.assert(t, annotations, err)
})
}
}
Loading