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
Publishing improvements: directory walking; prevent Vault unneeded version increment #602
Changes from 4 commits
8a216e9
ef68940
10ef21c
4254322
3ab2d41
02b0437
01b5fb6
ed31727
3ccc7e4
67f1654
3db9c71
0c6558b
0c26330
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -211,13 +211,21 @@ func main() { | |
}, | ||
{ | ||
Name: "publish", | ||
Usage: "Publish sops file to a configured destination", | ||
Usage: "Publish sops file or directory to a configured destination", | ||
ArgsUsage: `file`, | ||
Flags: append([]cli.Flag{ | ||
cli.BoolFlag{ | ||
Name: "yes, y", | ||
Usage: `pre-approve all changes and run non-interactively`, | ||
}, | ||
cli.BoolFlag{ | ||
Name: "omit-extensions", | ||
Usage: "Omit file extensions in destination path when publishing sops file to configured destinations", | ||
}, | ||
cli.BoolFlag{ | ||
Name: "recurse", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please change all mentions of this to recursive There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed in 3ab2d41 |
||
Usage: "If source path is directory, publish all its content recursively", | ||
mmorev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
cli.BoolFlag{ | ||
Name: "verbose", | ||
Usage: "Enable verbose logging output", | ||
|
@@ -235,14 +243,14 @@ func main() { | |
return common.NewExitError("Error: no file specified", codes.NoFileSpecified) | ||
} | ||
fileName := c.Args()[0] | ||
inputStore := inputStore(c, fileName) | ||
err = publishcmd.Run(publishcmd.Opts{ | ||
ConfigPath: configPath, | ||
InputPath: fileName, | ||
InputStore: inputStore, | ||
Cipher: aes.NewCipher(), | ||
KeyServices: keyservices(c), | ||
Interactive: !c.Bool("yes"), | ||
ConfigPath: configPath, | ||
InputPath: fileName, | ||
Cipher: aes.NewCipher(), | ||
KeyServices: keyservices(c), | ||
Interactive: !c.Bool("yes"), | ||
OmitExtensions: c.Bool("omit-extensions"), | ||
Recurse: c.Bool("recurse"), | ||
}) | ||
if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil { | ||
return cliErr | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import ( | |
"io/ioutil" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"go.mozilla.org/sops/v3" | ||
"go.mozilla.org/sops/v3/cmd/sops/codes" | ||
|
@@ -27,12 +28,14 @@ func init() { | |
|
||
// Opts represents publish options and config | ||
type Opts struct { | ||
Interactive bool | ||
Cipher sops.Cipher | ||
ConfigPath string | ||
InputPath string | ||
KeyServices []keyservice.KeyServiceClient | ||
InputStore sops.Store | ||
Interactive bool | ||
Cipher sops.Cipher | ||
ConfigPath string | ||
InputPath string | ||
KeyServices []keyservice.KeyServiceClient | ||
InputStore sops.Store | ||
OmitExtensions bool | ||
Recurse bool | ||
} | ||
|
||
// Run publish operation | ||
|
@@ -46,10 +49,27 @@ func Run(opts Opts) error { | |
if err != nil { | ||
return err | ||
} | ||
if info.IsDir() { | ||
if info.IsDir() && !opts.Recurse { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd nest this, if info.IsDir() {
if !opts.Recursive {
return fmt.Errorf("can't operate on a directory")
}
err = filepath.Walk(opts.InputPat...
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made some rework in 3ab2d41. See below. |
||
return fmt.Errorf("can't operate on a directory") | ||
} else if info.IsDir() && opts.Recurse { | ||
err = filepath.Walk(opts.InputPath, func(subPath string, info os.FileInfo, err error) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should handle the error passed into the function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed in 3ab2d41 |
||
subAbsPath, _ := filepath.Abs(subPath) | ||
if !info.IsDir() && subAbsPath != path { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for this comment. I just found out I had abused filepath.Walk function by using additional recursion inside it. So i moved out Walk call, IsDir check and store type detection to main.go: https://github.com/mozilla/sops/blob/3ab2d41c2f596e1fe5fa59c72a559514bcc91080/cmd/sops/main.go#L253 |
||
subOpts := opts | ||
subOpts.InputPath = subPath | ||
return Run(subOpts) | ||
} else { | ||
return nil | ||
} | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
_, fileName := filepath.Split(path) | ||
fileSuffix := filepath.Ext(path) | ||
opts.InputStore = common.DefaultStoreForPathOrFormat(path, fileSuffix) | ||
mmorev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
destinationPath := opts.InputPath | ||
|
||
conf, err := config.LoadDestinationRuleForFile(opts.ConfigPath, opts.InputPath, make(map[string]*string)) | ||
if err != nil { | ||
|
@@ -58,6 +78,9 @@ func Run(opts Opts) error { | |
if conf.Destination == nil { | ||
return errors.New("no destination configured for this file") | ||
} | ||
if opts.OmitExtensions || conf.OmitExtensions { | ||
destinationPath = strings.TrimSuffix(destinationPath, fileSuffix) | ||
} | ||
|
||
// Check that this is a sops-encrypted file | ||
tree, err := common.LoadEncryptedFile(opts.InputStore, opts.InputPath) | ||
|
@@ -146,22 +169,28 @@ func Run(opts Opts) error { | |
if opts.Interactive { | ||
var response string | ||
for response != "y" && response != "n" { | ||
fmt.Printf("uploading %s to %s ? (y/n): ", path, conf.Destination.Path(fileName)) | ||
fmt.Printf("uploading %s to %s ? (y/n): ", path, conf.Destination.Path(destinationPath)) | ||
_, err := fmt.Scanln(&response) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
if response == "n" { | ||
return errors.New("Publish canceled") | ||
msg := fmt.Sprintf("Publication of %s canceled", path) | ||
if opts.Recurse { | ||
fmt.Println(msg) | ||
return nil | ||
} else { | ||
return errors.New(msg) | ||
} | ||
} | ||
} | ||
|
||
switch dest := conf.Destination.(type) { | ||
case *publish.S3Destination, *publish.GCSDestination: | ||
err = dest.Upload(fileContents, fileName) | ||
err = dest.Upload(fileContents, destinationPath) | ||
case *publish.VaultDestination: | ||
err = dest.UploadUnencrypted(data, fileName) | ||
err = dest.UploadUnencrypted(data, destinationPath) | ||
} | ||
|
||
if err != nil { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import ( | |
"fmt" | ||
"strings" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
vault "github.com/hashicorp/vault/api" | ||
) | ||
|
||
|
@@ -65,6 +66,17 @@ func (vaultd *VaultDestination) UploadUnencrypted(data map[string]interface{}, f | |
} | ||
} | ||
|
||
existingSecret, err := client.Logical().Read(vaultd.secretsPath(fileName)) | ||
if err != nil { | ||
return err | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it might be good to keep going regardless (after printing a warning) if there's an error reading the secret. I'm imagining a situation where someone has given SOPS permission to only write to Vault. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea. Fixed this behavior in 01b5fb6: log warn when no read access, log info when data not changed |
||
} | ||
if existingSecret != nil { | ||
if cmp.Equal(data, existingSecret.Data["data"]) { | ||
fmt.Printf("Secret in %s is already up-to-date.\n", vaultd.secretsPath(fileName)) | ||
return nil | ||
} | ||
} | ||
|
||
secretsData := make(map[string]interface{}) | ||
|
||
if vaultd.kvVersion == 1 { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, what's your use case for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this question!
The goal is to use Vault as secrets source for several applications in different environments. As soon as it is a team work, we want to view diffs and approve/reject changes just like code changes in any Git platform (github, bitbucket) we do.
Vault has no option to stage changes like this (the only option in Enterprise edition is to approve the fact of write access, without seeing what data will be modified) and no diffs between versions or something. So we decided to store some info in Git/Sops and publish it to Vault in a batch triggered by repository change, and then lock down Vault to read-only mode by policies.
So in Vault we have some secrets schema, extensions are not needed there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for explaining, it's always good to see how people use sops :)