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

Allow P2F custom templates via ConfigMap #393

Merged
merged 3 commits into from
Dec 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 76 additions & 6 deletions PUSH_TO_FILE.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ Push to File operation.

- `/conjur/podinfo` for the `podinfo` volume.
- `/conjur/secrets` for the `conjur-secrets` volume.
- `/conjur/templates` for the `conjur-templates` volume.


```
Expand Down Expand Up @@ -263,6 +264,8 @@ Push to File operation.
mountPath: /conjur/podinfo
- name: conjur-secrets
mountPath: /conjur/secrets
- name: conjur-templates
mountPath: /conjur/templates
volumes:
- name: podinfo
downwardAPI:
Expand All @@ -273,6 +276,9 @@ Push to File operation.
- name: conjur-secrets
emptyDir:
medium: Memory
- name: conjur-templates
emptyDir:
medium: Memory
```

The Secrets Provider will create a secret file in the `conjur-secrets`
Expand Down Expand Up @@ -307,7 +313,7 @@ for a description of each environment variable setting:
| `conjur.org/conjur-secrets.{secret-group}` | Note\* | List of secrets to be retrieved from Conjur. Each entry can be either:<ul><li>A Conjur variable path</li><li> A key/value pairs of the form `<alias>:<Conjur variable path>` 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.<br><br>When this annotation is set, the policy paths defined by `conjur.org/conjur-secrets.{secret-group}` are relative to this common path.<br><br>When this annotation is not set, the policy paths defined by `conjur.org/conjur-secrets.{secret-group}` are themselves relative to the root policy.<br><br>(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.<br><br>If the `conjur.org/secret-file-template.{secret-group}` is set, then this secret file path must also be set, and it must include a file name (i.e. must not end in `/`).<br><br>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`.
| `conjur.org/secret-file-format.{secret-group}` | Note\* | Allowed values:<ul><li>yaml (default)</li><li>json</li><li>dotenv</li><li>bash</li></ul>(See [Example Secret File Formats](#example-secret-file-formats) for example output files.) |
| `conjur.org/secret-file-format.{secret-group}` | Note\* | Allowed values:<ul><li>yaml (default)</li><li>json</li><li>dotenv</li><li>bash</li><li>template</li></ul><br>This annotation must be set to `template` when using custom templates.<br><br>(See [Example Secret File Formats](#example-secret-file-formats) for example output files.) |
| `conjur.org/secret-file-template.{secret-group}`| Note\* | Defines a custom template in Golang text template format with which to render secret file content. See dedicated [Custom Templates for Secret Files](#custom-templates-for-secret-files) section for details. |

__Note*:__ These Push to File annotations do not have an equivalent
Expand Down Expand Up @@ -387,11 +393,75 @@ admin-password="dev/redis/password"
## Custom Templates for Secret Files

In addition to offering standard file formats, Push to File allows users to
define their own custom secret file templates, configured with the
`conjur.org/secret-file-template.{secret-group}` annotation. These templates
adhere to Go's text template formatting. Providing a custom template will
override the use of any standard format configured with the annotation
`conjur.org/secret-file-format.{secret-group}`.
define their own custom secret file templates. These templates adhere to Go's
text template formatting. Providing custom templates requires setting the
annotation `conjur.org/secret-file-format.{secret-group}` to `"template"`.
Custom templates can be provided either by explicit assignment through Pod
annotations, or through a volume-mounted ConfigMap.

1. <details><summary>Pod annotation</summary>

Custom templates can be defined with the Pod annotation
`conjur.org/secret-file-template.{secret-group}`. The following annotations
describe a valid secret group that uses a custom template defined in Pod
annotations:

```
conjur.org/secret-group.example: |
- admin-username: <variable-policy-path>
- admin-password: <variable-policy-path>
conjur.org/secret-file-format.example: "template"
conjur.org/secret-file-template.example: |
"database": {
"username": {{ secret "admin-username" }},
"password": {{ secret "admin-password" }},
}
```

</details>

2. <details><summary>Volume-mounted ConfigMap</summary>

Custom templates can be provided as fields in a ConfigMap. This feature
requires the following:

- The ConfigMap containing template files must be mounted in the Secrets
Provider container at `/conjur/templates`
- Each template's key in the ConfigMap's `data` field must be formatted as
`{secret-group}.tpl`.

The following is an example of a ConfigMap defining a custom Go template for
a secret group `example`:

```
apiVersion: v1
kind: ConfigMap
metadata:
name: my-custom-template
data:
example.tpl: |
"database": {
"username": {{ secret "admin-username" }},
"password": {{ secret "admin-password" }},
}
```

The following annotations describe a valid secret group that uses a custom
template defined in the above ConfigMap:

```
conjur.org/secret-group.example: |
- admin-username: <variable-policy-path>
- admin-password: <variable-policy-path>
conjur.org/secret-file-format.example: "template"
```

</details>

Providing a template with both methods for a single secret group fails before
retrieving secrets from Conjur.

### Using Conjur Secrets in Custom Templates

Injecting Conjur secrets into custom templates requires using the custom
template function `secret`. The action shown below renders the value associated
Expand Down
12 changes: 7 additions & 5 deletions cmd/secrets-provider/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
defaultContainerMode = "init"
annotationsFilePath = "/conjur/podinfo/annotations"
secretsBasePath = "/conjur/secrets"
templatesBasePath = "/conjur/templates"
)

var annotationsMap map[string]string
Expand Down Expand Up @@ -62,11 +63,12 @@ func main() {
}

providerConfig := secrets.ProviderConfig{
StoreType: secretsConfig.StoreType,
PodNamespace: secretsConfig.PodNamespace,
RequiredK8sSecrets: secretsConfig.RequiredK8sSecrets,
SecretFileBasePath: secretsBasePath,
AnnotationsMap: annotationsMap,
StoreType: secretsConfig.StoreType,
PodNamespace: secretsConfig.PodNamespace,
RequiredK8sSecrets: secretsConfig.RequiredK8sSecrets,
SecretFileBasePath: secretsBasePath,
TemplateFileBasePath: templatesBasePath,
AnnotationsMap: annotationsMap,
}
provideSecrets, errs := secrets.NewProviderForType(
secretRetriever.Retrieve,
Expand Down
2 changes: 1 addition & 1 deletion pkg/secrets/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ var pushToFileAnnotationPrefixes = map[string]annotationRestraints{
"conjur.org/conjur-secrets.": {TYPESTRING, []string{}},
"conjur.org/conjur-secrets-policy-path.": {TYPESTRING, []string{}},
"conjur.org/secret-file-path.": {TYPESTRING, []string{}},
"conjur.org/secret-file-format.": {TYPESTRING, []string{"yaml", "json", "dotenv", "bash"}},
"conjur.org/secret-file-format.": {TYPESTRING, []string{"yaml", "json", "dotenv", "bash", "template"}},
"conjur.org/secret-file-template": {TYPESTRING, []string{}},
}

Expand Down
6 changes: 4 additions & 2 deletions pkg/secrets/provide_conjur_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ type ProviderConfig struct {
RequiredK8sSecrets []string

// Config specific to Push to File provider
SecretFileBasePath string
AnnotationsMap map[string]string
SecretFileBasePath string
TemplateFileBasePath string
AnnotationsMap map[string]string
}

// ProviderFunc describes a function type responsible for providing secrets to an unspecified target.
Expand All @@ -50,6 +51,7 @@ func NewProviderForType(
provider, err := pushtofile.NewProvider(
secretsRetrieverFunc,
providerConfig.SecretFileBasePath,
providerConfig.TemplateFileBasePath,
providerConfig.AnnotationsMap,
)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/secrets/pushtofile/provide_conjur_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ type fileProvider struct {
}

// NewProvider creates a new provider for Push-to-File mode.
func NewProvider(retrieveSecretsFunc conjur.RetrieveSecretsFunc, secretsBasePath string, annotations map[string]string) (*fileProvider, []error) {
secretGroups, err := NewSecretGroups(secretsBasePath, annotations)
func NewProvider(retrieveSecretsFunc conjur.RetrieveSecretsFunc, secretsBasePath string, templatesBasePath string, annotations map[string]string) (*fileProvider, []error) {
secretGroups, err := NewSecretGroups(secretsBasePath, templatesBasePath, annotations)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/secrets/pushtofile/provide_conjur_secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestNewProvider(t *testing.T) {

for _, tc := range TestCases {
t.Run(tc.description, func(t *testing.T) {
p, err := NewProvider(tc.retrieveFunc, tc.basePath, tc.annotations)
p, err := NewProvider(tc.retrieveFunc, tc.basePath, "", tc.annotations)
assert.Empty(t, err)
assert.Equal(t, tc.expectedSecretGroup, p.secretGroups)
})
Expand Down
33 changes: 33 additions & 0 deletions pkg/secrets/pushtofile/pull_from_reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package pushtofile

import (
"io"
"io/ioutil"
"os"
)

type pullFromReaderFunc func(
reader io.Reader,
) (string, error)

type openReadCloserFunc func(
path string,
) (io.ReadCloser, error)

func openFileAsReadCloser(path string) (io.ReadCloser, error) {
reader, err := os.Open(path)
if err != nil {
return nil, err
}
return ioutil.NopCloser(reader), nil
}

func pullFromReader(
reader io.Reader,
) (string, error) {
content, err := ioutil.ReadAll(reader)
if err != nil {
return "", err
}
return string(content), nil
}
34 changes: 34 additions & 0 deletions pkg/secrets/pushtofile/pull_from_reader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package pushtofile

import (
"bytes"
"testing"
)

type pullFromReaderTestCase struct {
description string
content string
assert func(*testing.T, string, error)
}

func (tc pullFromReaderTestCase) Run(t *testing.T) {
t.Run(tc.description, func(t *testing.T) {
buf := bytes.NewBufferString(tc.content)
readContent, err := pullFromReader(buf)
tc.assert(t, readContent, err)
})
}

var pullFromReaderTestCases = []pullFromReaderTestCase{
{
description: "happy case",
content: "template file content",
assert: assertGoodOutput("template file content"),
},
}

func TestPullFromReader(t *testing.T) {
for _, tc := range pullFromReaderTestCases {
tc.Run(t)
}
}
59 changes: 59 additions & 0 deletions pkg/secrets/pushtofile/secret-group_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package pushtofile
import (
"bytes"
"io"
"io/ioutil"
"os"
)

Expand Down Expand Up @@ -76,3 +77,61 @@ func (spy *openWriteCloserSpy) Call(path string, permissions os.FileMode) (io.Wr

return spy.writeCloser, spy.err
}

//// pullFromReaderFunc
type pullFromReaderArgs struct {
reader io.Reader
}

type pullFromReaderSpy struct {
args pullFromReaderArgs
err error
_calls int
}

func (spy *pullFromReaderSpy) Call(
reader io.Reader,
) (string, error) {
spy._calls++
// This is to ensure the spy is only ever used once!
if spy._calls > 1 {
panic("spy called more than once")
}

spy.args = pullFromReaderArgs{
reader: reader,
}

content, err := ioutil.ReadAll(reader)
if err != nil {
return string(content), err
}

return string(content), spy.err
}

//// openReadCloserFunc
type openReadCloserArgs struct {
path string
}

type openReadCloserSpy struct {
args openReadCloserArgs
readCloser io.ReadCloser
err error
_calls int
}

func (spy *openReadCloserSpy) Call(path string) (io.ReadCloser, error) {
spy._calls++
// This is to ensure the spy is only ever used once!
if spy._calls > 1 {
panic("spy called more than once")
}

spy.args = openReadCloserArgs{
path: path,
}

return spy.readCloser, spy.err
}
Loading