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
TOOLS-2779: Add --config option for password values #62
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ package options | |
|
||
import ( | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
"regexp" | ||
"runtime" | ||
|
@@ -21,9 +22,11 @@ import ( | |
"github.com/mongodb/mongo-tools-common/failpoint" | ||
"github.com/mongodb/mongo-tools-common/log" | ||
"github.com/mongodb/mongo-tools-common/util" | ||
"github.com/pkg/errors" | ||
"go.mongodb.org/mongo-driver/mongo/readpref" | ||
"go.mongodb.org/mongo-driver/mongo/writeconcern" | ||
"go.mongodb.org/mongo-driver/x/mongo/driver/connstring" | ||
"gopkg.in/yaml.v2" | ||
) | ||
|
||
// XXX Force these true as the Go driver supports them always. Once the | ||
|
@@ -107,8 +110,9 @@ func (ns Namespace) String() string { | |
|
||
// Struct holding generic options | ||
type General struct { | ||
Help bool `long:"help" description:"print usage"` | ||
Version bool `long:"version" description:"print the tool version and exit"` | ||
Help bool `long:"help" description:"print usage"` | ||
Version bool `long:"version" description:"print the tool version and exit"` | ||
ConfigPath string `long:"config" description:"path to a configuration file with password secrets"` | ||
|
||
MaxProcs int `long:"numThreads" hidden:"true"` | ||
Failpoints string `long:"failpoints" hidden:"true"` | ||
|
@@ -423,9 +427,14 @@ func (opts *ToolOptions) AddOptions(extraOpts ExtraOptions) { | |
} | ||
} | ||
|
||
// Parse the command line args. Returns any extra args not accounted for by | ||
// parsing, as well as an error if the parsing returns an error. | ||
// ParseArgs parses a potential config file followed by the command line args, overriding | ||
// any values in the config file. Returns any extra args not accounted for by parsing, | ||
// as well as an error if the parsing returns an error. | ||
func (opts *ToolOptions) ParseArgs(args []string) ([]string, error) { | ||
if err := opts.ParseConfigFile(args); err != nil { | ||
return []string{}, err | ||
} | ||
|
||
args, err := opts.parser.ParseArgs(args) | ||
if err != nil { | ||
return []string{}, err | ||
|
@@ -452,6 +461,60 @@ func (opts *ToolOptions) ParseArgs(args []string) ([]string, error) { | |
return args, err | ||
} | ||
|
||
// ParseConfigFile iterates over args to find a --config option. If not found, we return. | ||
// If found, we read the contents of the specified config file in YAML format. We parse | ||
// any values corresponding to --password, --uri and --sslPEMKeyPassword, and store them | ||
// in the opts. | ||
func (opts *ToolOptions) ParseConfigFile(args []string) error { | ||
// Get config file path from the arguments, if specified. | ||
configPath := "" | ||
for i, arg := range args { | ||
if strings.HasPrefix(arg, "--config=") { | ||
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. Is there going to be a difference between 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. Hmm, interesting point. It seems the underlying call to From trying it on the command line as-is:
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 don't think it's the call to I think there are other issues here though. For example, does go-flags allow As I have noted in another PR, I'm generally skeptical of trying to match the library's flag-parsing behavior by manually scanning arguments |
||
// No value after equal sign. | ||
if len(arg) < 10 { | ||
return fmt.Errorf("--config option specified but no file given") | ||
} | ||
configPath = arg[9:] | ||
break | ||
} else if arg == "--config" { | ||
// No next argument. | ||
if i+1 == len(args) { | ||
return fmt.Errorf("--config option specified but no file given") | ||
} | ||
configPath = args[i+1] | ||
break | ||
} | ||
} | ||
|
||
// No --config option was found. | ||
if configPath == "" { | ||
return nil | ||
} | ||
|
||
configBytes, err := ioutil.ReadFile(configPath) | ||
if err != nil { | ||
return errors.Wrapf(err, "error opening file with --config") | ||
} | ||
|
||
// Unmarshal the config file as a top-level YAML file. | ||
var config struct { | ||
Password string `yaml:"password"` | ||
ConnectionString string `yaml:"uri"` | ||
SSLPEMKeyPassword string `yaml:"sslPEMKeyPassword"` | ||
} | ||
err = yaml.UnmarshalStrict(configBytes, &config) | ||
if err != nil { | ||
return errors.Wrapf(err, "error parsing config file %s", configPath) | ||
} | ||
|
||
// Assign each parsed value to its respective ToolOptions field. | ||
opts.Auth.Password = config.Password | ||
opts.URI.ConnectionString = config.ConnectionString | ||
opts.SSL.SSLPEMKeyPassword = config.SSLPEMKeyPassword | ||
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. To bring up a comment I made in the tech design a while ago -- before storing each field here, we could actually check to see if the fields were already set by the call to Might offer a bit of transparency but I don't feel strongly either way. 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 don't recommend doing that because the fields aren't necessarily directly set inside cmd parameters, for example, the password could be set inside connectionString. In that case, you won't be able to catch the overridden. 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. Right, it's true we wouldn't know if a password comes from It might be enough to just say something like 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. Since we already decided this wasn't a requirement for this project, I would also recommend that we not do this |
||
|
||
return nil | ||
} | ||
|
||
func (opts *ToolOptions) setURIFromPositionalArg(args []string) ([]string, error) { | ||
newArgs := []string{} | ||
var foundURI bool | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ package options | |
import ( | ||
"bytes" | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
"strings" | ||
|
||
|
@@ -550,6 +551,158 @@ func TestParseAndSetOptions(t *testing.T) { | |
}) | ||
} | ||
|
||
type configTester struct { | ||
description string | ||
yamlBytes []byte | ||
expectedOpts *ToolOptions | ||
outcome int | ||
} | ||
|
||
func runConfigFileTestCases(testCases []configTester) { | ||
configFilePath := "./test-config.yaml" | ||
args := []string{"--config", configFilePath} | ||
defer os.Remove(configFilePath) | ||
|
||
for _, testCase := range testCases { | ||
if err := ioutil.WriteFile(configFilePath, testCase.yamlBytes, 0644); err != nil { | ||
So(err, ShouldBeNil) | ||
} | ||
opts := New("test", "", "", "", false, EnabledOptions{true, true, true, true}) | ||
err := opts.ParseConfigFile(args) | ||
|
||
var assertion func() | ||
if testCase.outcome == ShouldSucceed { | ||
assertion = func() { | ||
So(err, ShouldBeNil) | ||
So(opts.Auth.Password, ShouldEqual, testCase.expectedOpts.Auth.Password) | ||
So(opts.URI.ConnectionString, ShouldEqual, testCase.expectedOpts.URI.ConnectionString) | ||
So(opts.SSL.SSLPEMKeyPassword, ShouldEqual, testCase.expectedOpts.SSL.SSLPEMKeyPassword) | ||
} | ||
} else { | ||
assertion = func() { | ||
So(err, ShouldNotBeNil) | ||
} | ||
} | ||
|
||
Convey(testCase.description, assertion) | ||
} | ||
} | ||
|
||
func createExpectedOpts(pw string, uri string, ssl string) *ToolOptions { | ||
opts := New("test", "", "", "", false, EnabledOptions{true, true, true, true}) | ||
opts.Auth.Password = pw | ||
opts.URI.ConnectionString = uri | ||
opts.SSL.SSLPEMKeyPassword = ssl | ||
return opts | ||
} | ||
|
||
func TestParseConfigFile(t *testing.T) { | ||
testtype.SkipUnlessTestType(t, testtype.UnitTestType) | ||
|
||
Convey("should error with no config file specified", t, func() { | ||
opts := New("test", "", "", "", false, EnabledOptions{}) | ||
|
||
// --config at beginning of args list | ||
args := []string{"--config", "--database", "myDB"} | ||
So(opts.ParseConfigFile(args), ShouldNotBeNil) | ||
|
||
// --config at end of args list | ||
args = []string{"--database", "myDB", "--config"} | ||
So(opts.ParseConfigFile(args), ShouldNotBeNil) | ||
|
||
// --config= at beginning of args list | ||
args = []string{"--config=", "--database", "myDB"} | ||
So(opts.ParseConfigFile(args), ShouldNotBeNil) | ||
|
||
// --config= at end of args list | ||
args = []string{"--database", "myDB", "--config="} | ||
So(opts.ParseConfigFile(args), ShouldNotBeNil) | ||
}) | ||
|
||
Convey("should error with non-existent config file specified", t, func() { | ||
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 the different positions here are overkill to just test a non-existent file. |
||
opts := New("test", "", "", "", false, EnabledOptions{}) | ||
|
||
// --config with non-existent file | ||
args := []string{"--config", "DoesNotExist.yaml", "--database", "myDB"} | ||
So(opts.ParseConfigFile(args), ShouldNotBeNil) | ||
|
||
// --config= with non-existent file | ||
args = []string{"--config=DoesNotExist.yaml", "--database", "myDB"} | ||
So(opts.ParseConfigFile(args), ShouldNotBeNil) | ||
}) | ||
|
||
Convey("with an existing config file specified", t, func() { | ||
runConfigFileTestCases([]configTester{ | ||
{ | ||
"containing nothing (empty file)", | ||
[]byte(""), | ||
createExpectedOpts("", "", ""), | ||
ShouldSucceed, | ||
}, | ||
{ | ||
"containing only password field", | ||
[]byte("password: abc123"), | ||
createExpectedOpts("abc123", "", ""), | ||
ShouldSucceed, | ||
}, | ||
{ | ||
"containing only uri field", | ||
[]byte("uri: abc123"), | ||
createExpectedOpts("", "abc123", ""), | ||
ShouldSucceed, | ||
}, | ||
{ | ||
"containing only sslPEMKeyPassword field", | ||
[]byte("sslPEMKeyPassword: abc123"), | ||
createExpectedOpts("", "", "abc123"), | ||
ShouldSucceed, | ||
}, | ||
{ | ||
"containing all of password, uri and sslPEMKeyPassword fields", | ||
[]byte("password: abc123\nuri: def456\nsslPEMKeyPassword: ghi789"), | ||
createExpectedOpts("abc123", "def456", "ghi789"), | ||
ShouldSucceed, | ||
}, | ||
{ | ||
"containing a duplicate field", | ||
[]byte("password: abc123\npassword: def456"), | ||
nil, | ||
ShouldFail, | ||
}, | ||
{ | ||
"containing an unsupported or misspelled field", | ||
[]byte("pasword: abc123"), | ||
nil, | ||
ShouldFail, | ||
}, | ||
}) | ||
}) | ||
|
||
Convey("with command line args that override config file values", t, func() { | ||
configFilePath := "./test-config.yaml" | ||
defer os.Remove(configFilePath) | ||
if err := ioutil.WriteFile(configFilePath, []byte("password: abc123"), 0644); err != nil { | ||
So(err, ShouldBeNil) | ||
} | ||
|
||
Convey("with --config followed by --password", func() { | ||
args := []string{"--config=" + configFilePath, "--password=def456"} | ||
opts := New("test", "", "", "", false, EnabledOptions{Auth: true}) | ||
_, err := opts.ParseArgs(args) | ||
So(err, ShouldBeNil) | ||
So(opts.Auth.Password, ShouldEqual, "def456") | ||
}) | ||
|
||
Convey("with --password followed by --config", func() { | ||
args := []string{"--password=ghi789", "--config=" + configFilePath} | ||
opts := New("test", "", "", "", false, EnabledOptions{Auth: true}) | ||
_, err := opts.ParseArgs(args) | ||
So(err, ShouldBeNil) | ||
So(opts.Auth.Password, ShouldEqual, "ghi789") | ||
}) | ||
}) | ||
} | ||
|
||
type optionsTester struct { | ||
options string | ||
uri string | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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.
I am wondering do we need
with password secrets
since later we could introduce more fields inside?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.
Yea, I don't think we really need it either. We could also drop that phrase later on when we do support more/all fields, but I'm not too attached to it. I can go ahead and remove it if @rychipman agrees.
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.
I think removing it makes sense!