Skip to content

Commit 36c7536

Browse files
authored
Merge pull request #144 from marco-m/recurse-upwards
Option to traverse up the directory hierarchy looking for secrets.yml
2 parents f8f022f + 94d8474 commit 36c7536

File tree

6 files changed

+149
-4
lines changed

6 files changed

+149
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ test.rb
88
test.out
99
junit.xml
1010
*.sublime-*
11+
.vscode/
1112
main
1213
keychain
1314
secrets.yml

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
66

77
## Unreleased
88

9+
### Added
10+
- Added ability to search for secrets.yml going up, starting from the current working directory
11+
[#122](https://github.com/cyberark/summon/issues/122)
12+
913
## [0.8.1] - 2020-03-02
1014
### Changed
1115
- Added ability to support empty variables [#124](https://github.com/cyberark/summon/issues/124)

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,20 @@ VARIABLE_WITH_DEFAULT: !var:default='defaultvalue' path/to/variable
136136

137137
`summon` supports a number of flags.
138138

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

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

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

146+
* `--up` searches for secrets.yml going up, starting from the current working
147+
directory.
148+
149+
Stops at the first file found or when the root of the current file system is
150+
reached. This allows to be at any directory depth in a project and simply do
151+
`summon -u <command>`.
152+
146153
* `-D 'var=value'` causes substitution of `value` to `$var`.
147154

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

159166
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.
160167

161-
* `-I, --ignore-all` A boolean to ignore any missing secret paths
168+
* `-I, --ignore-all` A boolean to ignore any missing secret paths.
162169

163170
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).
164171

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

167174
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:
168175

internal/command/action.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/exec"
88
"path"
9+
"path/filepath"
910
"strings"
1011
"sync"
1112
"syscall"
@@ -26,6 +27,7 @@ type ActionConfig struct {
2627
Ignores []string
2728
IgnoreAll bool
2829
Environment string
30+
RecurseUp bool
2931
ShowProviderVersions bool
3032
}
3133

@@ -56,6 +58,7 @@ var Action = func(c *cli.Context) {
5658
YamlInline: c.String("yaml"),
5759
Ignores: c.StringSlice("ignore"),
5860
IgnoreAll: c.Bool("ignore-all"),
61+
RecurseUp: c.Bool("up"),
5962
ShowProviderVersions: c.Bool("all-provider-versions"),
6063
Subs: convertSubsToMap(c.StringSlice("D")),
6164
})
@@ -87,6 +90,14 @@ func runAction(ac *ActionConfig) error {
8790
return nil
8891
}
8992

93+
if ac.RecurseUp {
94+
currentDir, err := os.Getwd()
95+
ac.Filepath, err = findInParentTree(ac.Filepath, currentDir)
96+
if err != nil {
97+
return err
98+
}
99+
}
100+
90101
switch ac.YamlInline {
91102
case "":
92103
secrets, err = secretsyml.ParseFromFile(ac.Filepath, ac.Environment, ac.Subs)
@@ -183,6 +194,43 @@ func joinEnv(env []string) string {
183194
return strings.Join(env, "\n") + "\n"
184195
}
185196

197+
// findInParentTree recursively searches for secretsFile starting at leafDir and in the
198+
// directories above leafDir until it is found or the root of the file system is reached.
199+
// If found, returns the absolute path to the file.
200+
func findInParentTree(secretsFile string, leafDir string) (string, error) {
201+
if filepath.IsAbs(secretsFile) {
202+
return "", fmt.Errorf(
203+
"file specified (%s) is an absolute path: will not recurse up", secretsFile)
204+
}
205+
206+
for {
207+
joinedPath := filepath.Join(leafDir, secretsFile)
208+
209+
_, err := os.Stat(joinedPath)
210+
211+
if err != nil {
212+
// If the file is not present, we just move up one level and run the next loop
213+
// iteration
214+
if os.IsNotExist(err) {
215+
upOne := filepath.Dir(leafDir)
216+
if upOne == leafDir {
217+
return "", fmt.Errorf(
218+
"unable to locate file specified (%s): reached root of file system", secretsFile)
219+
}
220+
221+
leafDir = upOne
222+
continue
223+
}
224+
225+
// If we have an unexpected error, we fail-fast
226+
return "", fmt.Errorf("unable to locate file specified (%s): %s", secretsFile, err)
227+
}
228+
229+
// If there's no error, we found the file so we return it
230+
return joinedPath, nil
231+
}
232+
}
233+
186234
// scans arguments for the magic string; if found,
187235
// creates a tempfile to which all the environment mappings are dumped
188236
// and replaces the magic string with its path.

internal/command/action_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ package command
22

33
import (
44
"errors"
5+
"fmt"
56
"io/ioutil"
67
"os"
78
"os/exec"
89
"path"
910
"path/filepath"
11+
"strconv"
1012
"strings"
1113
"testing"
14+
"time"
1215

1316
"github.com/cyberark/summon/secretsyml"
1417
. "github.com/smartystreets/goconvey/convey"
@@ -240,3 +243,81 @@ testprovider-trailingnewline version 3.2.1
240243
So(output, ShouldEqual, expected)
241244
})
242245
}
246+
247+
func TestLocateFileRecurseUp(t *testing.T) {
248+
filename := "test.txt"
249+
250+
Convey("Finds file in current working directory", t, func() {
251+
topDir, err := ioutil.TempDir("", "summon")
252+
So(err, ShouldBeNil)
253+
defer os.RemoveAll(topDir)
254+
255+
localFilePath := filepath.Join(topDir, filename)
256+
_, err = os.Create(localFilePath)
257+
So(err, ShouldBeNil)
258+
259+
gotPath, err := findInParentTree(filename, topDir)
260+
So(err, ShouldBeNil)
261+
262+
So(gotPath, ShouldEqual, localFilePath)
263+
})
264+
265+
Convey("Finds file in a higher working directory", t, func() {
266+
topDir, err := ioutil.TempDir("", "summon")
267+
So(err, ShouldBeNil)
268+
defer os.RemoveAll(topDir)
269+
270+
higherFilePath := filepath.Join(topDir, filename)
271+
_, err = os.Create(higherFilePath)
272+
So(err, ShouldBeNil)
273+
274+
// Create a downwards directory hierarchy, starting from topDir
275+
downDir := filepath.Join(topDir, "dir1", "dir2", "dir3")
276+
err = os.MkdirAll(downDir, 0700)
277+
So(err, ShouldBeNil)
278+
279+
gotPath, err := findInParentTree(filename, downDir)
280+
So(err, ShouldBeNil)
281+
282+
So(gotPath, ShouldEqual, higherFilePath)
283+
})
284+
285+
Convey("returns a friendly error if file not found", t, func() {
286+
topDir, err := ioutil.TempDir("", "summon")
287+
So(err, ShouldBeNil)
288+
defer os.RemoveAll(topDir)
289+
290+
// A unlikely to exist file name
291+
nonExistingFileName := strconv.FormatInt(time.Now().Unix(), 10)
292+
wantErrMsg := fmt.Sprintf(
293+
"unable to locate file specified (%s): reached root of file system",
294+
nonExistingFileName)
295+
296+
_, err = findInParentTree(nonExistingFileName, topDir)
297+
So(err.Error(), ShouldEqual, wantErrMsg)
298+
})
299+
300+
Convey("returns a friendly error if file is an absolute path", t, func() {
301+
topDir, err := ioutil.TempDir("", "summon")
302+
So(err, ShouldBeNil)
303+
defer os.RemoveAll(topDir)
304+
305+
absFileName := "/foo/bar/baz"
306+
wantErrMsg := "file specified (/foo/bar/baz) is an absolute path: will not recurse up"
307+
308+
_, err = findInParentTree(absFileName, topDir)
309+
So(err.Error(), ShouldEqual, wantErrMsg)
310+
})
311+
312+
Convey("returns a friendly error in unexpected circumstances (100% coverage)", t, func() {
313+
topDir, err := ioutil.TempDir("", "summon")
314+
So(err, ShouldBeNil)
315+
defer os.RemoveAll(topDir)
316+
317+
fileNameWithNulByte := "pizza\x00margherita"
318+
wantErrMsg := "unable to locate file specified (pizza\x00margherita): stat"
319+
320+
_, err = findInParentTree(fileNameWithNulByte, topDir)
321+
So(err.Error(), ShouldStartWith, wantErrMsg)
322+
})
323+
}

internal/command/flags.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ var Flags = []cli.Flag{
1919
Value: "secrets.yml",
2020
Usage: "Path to secrets.yml",
2121
},
22+
cli.BoolFlag{
23+
Name: "up",
24+
Usage: "Go up in the directory hierarchy until the secrets file is found",
25+
},
2226
cli.StringSliceFlag{
2327
Name: "D",
2428
Value: &cli.StringSlice{},

0 commit comments

Comments
 (0)