Skip to content
This repository has been archived by the owner on May 3, 2022. It is now read-only.

Commit

Permalink
feat: simplify the credentialset logic (#305)
Browse files Browse the repository at this point in the history
* feat: simplify the credentialset logic

Closes #266

* docs: update with new credential resolution

* fix: fix linting errors

* fix: rewrite a test to pass the formatting linter

* fix: fixed docs and removed unused fields.
  • Loading branch information
technosophos authored Oct 31, 2018
1 parent 423b119 commit 99bf8d8
Show file tree
Hide file tree
Showing 16 changed files with 218 additions and 163 deletions.
14 changes: 3 additions & 11 deletions cmd/duffle/credential_generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ func newCredentialGenerateCmd(out io.Writer) *cobra.Command {
}

creds := genCredentialSet(csName, bun.Credentials)
//data, err := json.MarshalIndent(creds, "", " ")
data, err := yaml.Marshal(creds)
if err != nil {
return err
Expand All @@ -77,17 +76,10 @@ func genCredentialSet(name string, creds map[string]bundle.CredentialLocation) c
}
cs.Credentials = []credentials.CredentialStrategy{}

for name, loc := range creds {
for name := range creds {
c := credentials.CredentialStrategy{
Name: name,
Source: credentials.Source{Value: "EMPTY"},
Destination: credentials.Destination{},
}
if loc.EnvironmentVariable != "" {
c.Destination.EnvVar = loc.EnvironmentVariable
}
if loc.Path != "" {
c.Destination.Path = loc.Path
Name: name,
Source: credentials.Source{Value: "EMPTY"},
}
cs.Credentials = append(cs.Credentials, c)
}
Expand Down
18 changes: 9 additions & 9 deletions cmd/duffle/credential_generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ func TestGenCredentialSet(t *testing.T) {
is.Equal(creds.Name, name)
is.Len(creds.Credentials, 2)

found := map[string]bool{"first": false, "second": false}

for _, cred := range creds.Credentials {
if cred.Name == "first" {
is.Equal(cred.Destination.EnvVar, credlocs["first"].EnvironmentVariable)
is.Equal(cred.Source.Value, "EMPTY")
} else if cred.Name == "second" {
is.Equal(cred.Destination.EnvVar, credlocs["second"].EnvironmentVariable)
is.Equal(cred.Destination.Path, credlocs["second"].Path)
} else {
t.Fatalf("unexpected credential %s", cred.Name)
}
found[cred.Name] = true
is.Equal(cred.Source.Value, "EMPTY")
}

is.Len(found, 2)
for k, v := range found {
is.True(v, "%q not found", k)
}
}
3 changes: 0 additions & 3 deletions cmd/duffle/credential_show.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@ func (sh *credentialShowCmd) printCredentials(cs credentials.CredentialSet) erro
if cred.Source.Value != "" {
cred.Source.Value = "REDACTED"
}
if cred.Destination.Value != "" {
cred.Destination.Value = "REDACTED"
}
creds[i] = cred
}
cs.Credentials = creds
Expand Down
50 changes: 15 additions & 35 deletions cmd/duffle/credential_show_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,19 @@ func TestPrintCredentials(t *testing.T) {
Name: "foo",
Credentials: []credentials.CredentialStrategy{
{
Name: "password",
Source: credentials.Source{Value: "TOPSECRET"},
Destination: credentials.Destination{EnvVar: "PASSWORD"},
Name: "password",
Source: credentials.Source{Value: "TOPSECRET"},
},
{
Name: "another-password",
Destination: credentials.Destination{Value: "TOPSECRET"},
Name: "another-password",
},
{
Name: "kubeconfig",
Source: credentials.Source{Path: "/root/.kube/config"},
Destination: credentials.Destination{Path: "/root/.kube/config"},
Name: "kubeconfig",
Source: credentials.Source{Path: "/root/.kube/config"},
},
{
Name: "some-setting",
Source: credentials.Source{EnvVar: "MYSETTING"},
Destination: credentials.Destination{EnvVar: "MYSETTING"},
Name: "some-setting",
Source: credentials.Source{EnvVar: "MYSETTING"},
},
},
}
Expand All @@ -40,45 +36,29 @@ func TestPrintCredentials(t *testing.T) {
}{
{name: "reacted", unredacted: false, output: `name: foo
credentials:
- destination:
env: PASSWORD
name: password
- name: password
source:
value: REDACTED
- destination:
value: REDACTED
name: another-password
- name: another-password
source: {}
- destination:
path: /root/.kube/config
name: kubeconfig
- name: kubeconfig
source:
path: /root/.kube/config
- destination:
env: MYSETTING
name: some-setting
- name: some-setting
source:
env: MYSETTING
`},
{name: "unredacted", unredacted: true, output: `name: foo
credentials:
- destination:
env: PASSWORD
name: password
- name: password
source:
value: TOPSECRET
- destination:
value: TOPSECRET
name: another-password
- name: another-password
source: {}
- destination:
path: /root/.kube/config
name: kubeconfig
- name: kubeconfig
source:
path: /root/.kube/config
- destination:
env: MYSETTING
name: some-setting
- name: some-setting
source:
env: MYSETTING
`},
Expand Down
4 changes: 2 additions & 2 deletions cmd/duffle/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ func claimStorage() claim.Store {
}

// loadCredentials loads a set of credentials from HOME.
func loadCredentials(file string, b *bundle.Bundle) (map[string]credentials.Destination, error) {
creds := map[string]credentials.Destination{}
func loadCredentials(file string, b *bundle.Bundle) (map[string]string, error) {
creds := map[string]string{}
if file == "" {
return creds, credentials.Validate(creds, b.Credentials)
}
Expand Down
10 changes: 6 additions & 4 deletions cmd/duffle/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import (

func TestIsPathy(t *testing.T) {
is := assert.New(t)
thispath := filepath.Join("this", "is", "a", "path")
fooya := filepath.Join("..", "foo.yaml")
for path, expect := range map[string]bool{
"foo": false,
filepath.Join("this", "is", "a", "path"): true,
"foo.yaml": false,
filepath.Join("..", "foo.yaml"): true,
"foo": false,
thispath: true,
"foo.yaml": false,
fooya: true,
} {
is.Equal(expect, isPathy(path), "Expected %t, for %s", expect, path)
}
Expand Down
86 changes: 57 additions & 29 deletions docs/201-credentialset.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

This document covers how credentials are passed into Duffle from the environment.

> This functionality was not part of the initial draft specification.
## The Credential Problem

Consider the case where a CNAB bundle named `example/myapp:1.0.0` connects to both ARM (Azure IaaS API service) and Kubernetes. Each has its own API surface which is secured by a separate set of credentials. ARM requires a periodically expiring token managed via `az`. Kubernetes stores credentialing information in a `KUBECONFIG` YAML file.
Expand Down Expand Up @@ -52,33 +50,21 @@ credentials:
source:
path: $SOMEPATH/testdata/someconfig.txt # credential will be read from this file
# In 'path', env vars are evaluated.
destination:
# credential data will be presented as environment variable $TEST_READ_FILE
env: TEST_READ_FILE
- name: run_program
source:
command: "echo wildebeest" # The command `echo wildebeest` will be executed
# An error will cause the process to exit
destination:
env: TEST_RUN_PROGRAM # Results will be placed as an env var.
- name: use_var
source:
env: TEST_USE_VAR # This will read an env var from local, and copy to dest
value: "this space intentionally left non-blank"
destination:
env: TEST_USE_VAR
- name: fallthrough
source:
name: NO_SUCH_VAR # Assuming this is not set....
value: quokka # Then this will be used as the default value
destination:
env: TEST_FALLTHROUGH # The result will be written to env var...
path: animals/quokka.txt # and also to a file path.
- name: plain_value
source:
value: cassowary # Load this literal value.
destination:
path: animals/cassowary.txt # Save the value to a file on dest.
```
The above shows several examples of how credentials can be loaded from a local source and
Expand All @@ -91,12 +77,7 @@ Loading from source is done from four potential inputs:
- `path` is loaded from a file at the given path (or else it errors)
- `command` executes a command, and returns the output as the value (or else it errors)

Data can then be passed into the image in one of two ways:

- `env` will store the data as an environment variable
- `path` will store the data as the contents of a file located at the given path

Note that both `env` and `path` can be specified, which will result in the data being stored in both.
Duffle will capture (at runtime) the data presented by these sources, and will pass the data into the container as required.

Credential sets are specified when needed:

Expand All @@ -108,22 +89,69 @@ $ duffle install --credentials=staging my_example example/myapp:1.0.0

Credential sets are loaded locally. All commands are executed locally. Then the results are injected into the image at startup.

## Default Credential Sets
## Matching Credentials in a Bundle

Bundles declare which credentials they require. This information is specified in the `bundle.json`:

```json
{
"schemaVersion": 1,
"name": "helloworld",
"version": "0.1.2",
"description": "An example 'thin' helloworld Cloud-Native Application Bundle",
"invocationImages":[],
"images": [],
"parameters": {},
"credentials": {
"kubeconfig": {
"path": "/home/.kube/config",
},
"image_token": {
"env": "AZ_IMAGE_TOKEN",
},
"hostkey": {
"path": "/etc/hostkey.txt",
"env": "HOST_KEY"
}
}
}
```

The `credentials` section maps a name (e.g. `kubeconfig`) to the destination (e.g. `path: ...` or `env: ...`).

A default credential set may be specified in the Duffle preferences. They may be set in a project's `duffle.yaml`, or (more frequently) per user in the `$HOME/.duffle/preferences.yaml` file:
Duffle will match the credentials requested in the `bundle.json` to the credentials specified in the credential set passed with the `--credentials` flag. Matching is done by name. Thus, to send configuration data to the above bundle, we would need a credentialset like this:

```yaml
defaultCredentials: "staging"
name: mycreds
credentials:
- name: kubeconfig
source:
path: $HOME/.kube/config
- name: image_token
source:
value: "abcdefg"
- name: hostkey
source:
env: "HOSTKEY"
- name: sir-not-appearing-in-this-film
source:
value: unused
```

## Limitations
When the above bundle (`helloworld`) is installed with the above credentials (`mycreds`), the credentials are resolved as follows:

In this model, credentials can only be injected as files and environment variables. Some systems may not be satisfied with this limitation, in which case additional scripting may be required inside of the invocation image.
- `kubeconfig` is read from Duffle's local path (`$HOME/.kube/config`), and the contents are placed into the invocation image at the path `/home/.kube/config`
- `image_token` is treated as a literal value, and the string `abcdefg` is injected into the invocation image as the environment variable `$AZ_IMAGE_TOKEN`
- `hostkey` is read from the local environment variable `$HOSTKEY`, and is then injected into two places in the invocation image:
- It is set as the value of `$HOST_KEY`
- It is placed in the file `/etc/hostkey.txt`

Other:
Since the last credential in the credential set (`sir-not-appearing-in-this-film`) is not required by the bundle, it is ignored.

- We might be able to put all credentials in one large YAML file. Credentials may include x509 certs or other large things
- There is no way to specify in a CNAB bundle what credentials are required on the host system, other than by documentation. We might have to figure that out at some point
- We don't address how a credential might be injected into a file on the image. The assumption is that such a thing would be scripted
During the resolution phase, _if any required credential is not provided in the credential set, the operation is aborted and Duffle exits with a failure._

## Limitations

In this model, credentials can only be injected as files and environment variables. Some systems may not be satisfied with this limitation, in which case additional scripting may be required inside of the invocation image.

Next Section: [drivers](202-drivers.md)
7 changes: 3 additions & 4 deletions pkg/action/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,11 @@ func selectInvocationImage(d driver.Driver, c *claim.Claim) (bundle.InvocationIm
return bundle.InvocationImage{}, errors.New("driver is not compatible with any of the invocation images in the bundle")
}

func opFromClaim(action string, c *claim.Claim, ii bundle.InvocationImage, creds credentials.Set, w io.Writer) *driver.Operation {
env, files := creds.Flatten()
func opFromClaim(action string, c *claim.Claim, ii bundle.InvocationImage, creds credentials.Set, w io.Writer) (*driver.Operation, error) {
env, files, err := creds.Expand(c.Bundle)
for k, v := range c.Files {
files[c.Bundle.Files[k].Path] = v
}

return &driver.Operation{
Action: action,
Installation: c.Name,
Expand All @@ -55,7 +54,7 @@ func opFromClaim(action string, c *claim.Claim, ii bundle.InvocationImage, creds
Environment: conflateEnv(action, c, env),
Files: files,
Out: w,
}
}, err
}

// conflateEnv combines all the stuff that should be placed into env vars
Expand Down
23 changes: 14 additions & 9 deletions pkg/action/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,8 @@ type mockFailingDriver struct {
}

var mockSet = credentials.Set{
"secret_one": {
EnvVar: "SECRET_ONE",
Value: "I'm a secret",
},
"secret_two": {
Path: "secret_two",
Value: "I'm also a secret",
},
"secret_one": "I'm a secret",
"secret_two": "I'm also a secret",
}

func (d *mockFailingDriver) Handles(imageType string) bool {
Expand All @@ -44,6 +38,14 @@ func mockBundle() *bundle.Bundle {
InvocationImages: []bundle.InvocationImage{
{Image: "foo/bar:0.1.0", ImageType: "docker"},
},
Credentials: map[string]bundle.CredentialLocation{
"secret_one": {
EnvironmentVariable: "SECRET_ONE",
},
"secret_two": {
Path: "secret_two",
},
},
}

}
Expand All @@ -60,7 +62,10 @@ func TestOpFromClaim(t *testing.T) {
}
invocImage := c.Bundle.InvocationImages[0]

op := opFromClaim(claim.ActionInstall, c, invocImage, mockSet, os.Stdout)
op, err := opFromClaim(claim.ActionInstall, c, invocImage, mockSet, os.Stdout)
if err != nil {
t.Fatal(err)
}

is := assert.New(t)

Expand Down
5 changes: 4 additions & 1 deletion pkg/action/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ func (i *Install) Run(c *claim.Claim, creds credentials.Set, w io.Writer) error
return err
}

op := opFromClaim(claim.ActionInstall, c, invocImage, creds, w)
op, err := opFromClaim(claim.ActionInstall, c, invocImage, creds, w)
if err != nil {
return err
}
if err := i.Driver.Run(op); err != nil {
c.Update(claim.ActionInstall, claim.StatusFailure)
c.Result.Message = err.Error()
Expand Down
Loading

0 comments on commit 99bf8d8

Please sign in to comment.