Skip to content

Commit

Permalink
TOOLS-2779: Add --config option for password values (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
edobranov committed Dec 14, 2020
1 parent 8937e29 commit 56f379e
Show file tree
Hide file tree
Showing 23 changed files with 10,242 additions and 4 deletions.
9 changes: 9 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Expand Up @@ -32,6 +32,10 @@
name = "github.com/jessevdk/go-flags"
version = "^v1.4.0"

[[constraint]]
name = "gopkg.in/yaml.v2"
version = "^v2.3.0"

[[constraint]]
name = "github.com/smartystreets/goconvey"
revision = "bf58a9a1291224109919756b4dcc469c670cc7e4"
Expand Down
58 changes: 54 additions & 4 deletions options/options.go
Expand Up @@ -10,6 +10,7 @@ package options

import (
"fmt"
"io/ioutil"
"os"
"regexp"
"runtime"
Expand All @@ -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
Expand Down Expand Up @@ -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"`

MaxProcs int `long:"numThreads" hidden:"true"`
Failpoints string `long:"failpoints" hidden:"true"`
Expand Down Expand Up @@ -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
Expand All @@ -452,6 +461,47 @@ 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.
_, err := opts.parser.ParseArgs(args)
if err != nil {
return err
}

// No --config option was specified.
if opts.General.ConfigPath == "" {
return nil
}

// --config option specifies a file path.
configBytes, err := ioutil.ReadFile(opts.General.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", opts.General.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

return nil
}

func (opts *ToolOptions) setURIFromPositionalArg(args []string) ([]string, error) {
newArgs := []string{}
var foundURI bool
Expand Down
153 changes: 153 additions & 0 deletions options/options_test.go
Expand Up @@ -9,6 +9,7 @@ package options
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"strings"

Expand Down Expand Up @@ -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() {
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
Expand Down
16 changes: 16 additions & 0 deletions vendor/gopkg.in/yaml.v2/.travis.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 56f379e

Please sign in to comment.