Skip to content

Commit

Permalink
Merge pull request #144 from marco-m/recurse-upwards
Browse files Browse the repository at this point in the history
Option to traverse up the directory hierarchy looking for secrets.yml
  • Loading branch information
sgnn7 authored Mar 25, 2020
2 parents f8f022f + 94d8474 commit 36c7536
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ test.rb
test.out
junit.xml
*.sublime-*
.vscode/
main
keychain
secrets.yml
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## Unreleased

### Added
- Added ability to search for secrets.yml going up, starting from the current working directory
[#122](https://github.com/cyberark/summon/issues/122)

## [0.8.1] - 2020-03-02
### Changed
- Added ability to support empty variables [#124](https://github.com/cyberark/summon/issues/124)
Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,20 @@ VARIABLE_WITH_DEFAULT: !var:default='defaultvalue' path/to/variable

`summon` supports a number of flags.

* `-p, --provider` specify the path to the [provider](provider/README.md) summon should use
* `-p, --provider` specify the path to the [provider](provider/README.md) summon should use.

If the provider is in the default path, `/usr/local/lib/summon/` you can just
provide the name of the executable. If not, use a full path.

* `-f <path>` specify a location to a secrets.yml file, default 'secrets.yml' in current directory.

* `--up` searches for secrets.yml going up, starting from the current working
directory.

Stops at the first file found or when the root of the current file system is
reached. This allows to be at any directory depth in a project and simply do
`summon -u <command>`.

* `-D 'var=value'` causes substitution of `value` to `$var`.

You can use the same secrets.yml file for different environments, using `-D` to
Expand All @@ -154,15 +161,15 @@ VARIABLE_WITH_DEFAULT: !var:default='defaultvalue' path/to/variable
summon -D ENV=production --yaml 'SQL_PASSWORD: !var env/$ENV/db-password' deploy.sh
```
* `-i, --ignore` A secret path for which to ignore provider errors
* `-i, --ignore` A secret path for which to ignore provider errors.

This flag can be useful for when you have secrets that you don't need access to for development. For example API keys for monitoring tools. This flag can be used multiple times.

* `-I, --ignore-all` A boolean to ignore any missing secret paths
* `-I, --ignore-all` A boolean to ignore any missing secret paths.

This flag can be useful when the underlying system that's going to be using the values implements defaults. For example, when using summon as a bridge to [confd](https://github.com/kelseyhightower/confd).

* `-e, --environment` Specify section (environment) to parse from secret YAML
* `-e, --environment` Specify section (environment) to parse from secret YAML.

This flag specifies which specific environment/section to parse from the secrets YAML file (or string). In addition, it will also enable the usage of a `common` (or `default`) section which will be inherited by other sections/environments. In other words, if your `secrets.yaml` looks something like this:

Expand Down
48 changes: 48 additions & 0 deletions internal/command/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"sync"
"syscall"
Expand All @@ -26,6 +27,7 @@ type ActionConfig struct {
Ignores []string
IgnoreAll bool
Environment string
RecurseUp bool
ShowProviderVersions bool
}

Expand Down Expand Up @@ -56,6 +58,7 @@ var Action = func(c *cli.Context) {
YamlInline: c.String("yaml"),
Ignores: c.StringSlice("ignore"),
IgnoreAll: c.Bool("ignore-all"),
RecurseUp: c.Bool("up"),
ShowProviderVersions: c.Bool("all-provider-versions"),
Subs: convertSubsToMap(c.StringSlice("D")),
})
Expand Down Expand Up @@ -87,6 +90,14 @@ func runAction(ac *ActionConfig) error {
return nil
}

if ac.RecurseUp {
currentDir, err := os.Getwd()
ac.Filepath, err = findInParentTree(ac.Filepath, currentDir)
if err != nil {
return err
}
}

switch ac.YamlInline {
case "":
secrets, err = secretsyml.ParseFromFile(ac.Filepath, ac.Environment, ac.Subs)
Expand Down Expand Up @@ -183,6 +194,43 @@ func joinEnv(env []string) string {
return strings.Join(env, "\n") + "\n"
}

// findInParentTree recursively searches for secretsFile starting at leafDir and in the
// directories above leafDir until it is found or the root of the file system is reached.
// If found, returns the absolute path to the file.
func findInParentTree(secretsFile string, leafDir string) (string, error) {
if filepath.IsAbs(secretsFile) {
return "", fmt.Errorf(
"file specified (%s) is an absolute path: will not recurse up", secretsFile)
}

for {
joinedPath := filepath.Join(leafDir, secretsFile)

_, err := os.Stat(joinedPath)

if err != nil {
// If the file is not present, we just move up one level and run the next loop
// iteration
if os.IsNotExist(err) {
upOne := filepath.Dir(leafDir)
if upOne == leafDir {
return "", fmt.Errorf(
"unable to locate file specified (%s): reached root of file system", secretsFile)
}

leafDir = upOne
continue
}

// If we have an unexpected error, we fail-fast
return "", fmt.Errorf("unable to locate file specified (%s): %s", secretsFile, err)
}

// If there's no error, we found the file so we return it
return joinedPath, nil
}
}

// scans arguments for the magic string; if found,
// creates a tempfile to which all the environment mappings are dumped
// and replaces the magic string with its path.
Expand Down
81 changes: 81 additions & 0 deletions internal/command/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package command

import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"testing"
"time"

"github.com/cyberark/summon/secretsyml"
. "github.com/smartystreets/goconvey/convey"
Expand Down Expand Up @@ -240,3 +243,81 @@ testprovider-trailingnewline version 3.2.1
So(output, ShouldEqual, expected)
})
}

func TestLocateFileRecurseUp(t *testing.T) {
filename := "test.txt"

Convey("Finds file in current working directory", t, func() {
topDir, err := ioutil.TempDir("", "summon")
So(err, ShouldBeNil)
defer os.RemoveAll(topDir)

localFilePath := filepath.Join(topDir, filename)
_, err = os.Create(localFilePath)
So(err, ShouldBeNil)

gotPath, err := findInParentTree(filename, topDir)
So(err, ShouldBeNil)

So(gotPath, ShouldEqual, localFilePath)
})

Convey("Finds file in a higher working directory", t, func() {
topDir, err := ioutil.TempDir("", "summon")
So(err, ShouldBeNil)
defer os.RemoveAll(topDir)

higherFilePath := filepath.Join(topDir, filename)
_, err = os.Create(higherFilePath)
So(err, ShouldBeNil)

// Create a downwards directory hierarchy, starting from topDir
downDir := filepath.Join(topDir, "dir1", "dir2", "dir3")
err = os.MkdirAll(downDir, 0700)
So(err, ShouldBeNil)

gotPath, err := findInParentTree(filename, downDir)
So(err, ShouldBeNil)

So(gotPath, ShouldEqual, higherFilePath)
})

Convey("returns a friendly error if file not found", t, func() {
topDir, err := ioutil.TempDir("", "summon")
So(err, ShouldBeNil)
defer os.RemoveAll(topDir)

// A unlikely to exist file name
nonExistingFileName := strconv.FormatInt(time.Now().Unix(), 10)
wantErrMsg := fmt.Sprintf(
"unable to locate file specified (%s): reached root of file system",
nonExistingFileName)

_, err = findInParentTree(nonExistingFileName, topDir)
So(err.Error(), ShouldEqual, wantErrMsg)
})

Convey("returns a friendly error if file is an absolute path", t, func() {
topDir, err := ioutil.TempDir("", "summon")
So(err, ShouldBeNil)
defer os.RemoveAll(topDir)

absFileName := "/foo/bar/baz"
wantErrMsg := "file specified (/foo/bar/baz) is an absolute path: will not recurse up"

_, err = findInParentTree(absFileName, topDir)
So(err.Error(), ShouldEqual, wantErrMsg)
})

Convey("returns a friendly error in unexpected circumstances (100% coverage)", t, func() {
topDir, err := ioutil.TempDir("", "summon")
So(err, ShouldBeNil)
defer os.RemoveAll(topDir)

fileNameWithNulByte := "pizza\x00margherita"
wantErrMsg := "unable to locate file specified (pizza\x00margherita): stat"

_, err = findInParentTree(fileNameWithNulByte, topDir)
So(err.Error(), ShouldStartWith, wantErrMsg)
})
}
4 changes: 4 additions & 0 deletions internal/command/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ var Flags = []cli.Flag{
Value: "secrets.yml",
Usage: "Path to secrets.yml",
},
cli.BoolFlag{
Name: "up",
Usage: "Go up in the directory hierarchy until the secrets file is found",
},
cli.StringSliceFlag{
Name: "D",
Value: &cli.StringSlice{},
Expand Down

0 comments on commit 36c7536

Please sign in to comment.