From cf3eac0242e1ec8356fd298e5b9d38da6ea678b3 Mon Sep 17 00:00:00 2001 From: Joel Speed Date: Sat, 8 Jun 2019 22:20:18 +0200 Subject: [PATCH] Init Viper for config with defaulting --- Gopkg.lock | 99 ++++++++++++++++++++++++++++++++ pkg/apis/options/cookie.go | 36 +++++++++--- pkg/apis/options/default.go | 68 ++++++++++++++++++++++ pkg/apis/options/default_test.go | 60 +++++++++++++++++++ pkg/apis/options/options.go | 66 +++++++++++++++++++++ pkg/apis/options/options_test.go | 37 ++++++++++++ 6 files changed, 357 insertions(+), 9 deletions(-) create mode 100644 pkg/apis/options/default.go create mode 100644 pkg/apis/options/default_test.go create mode 100644 pkg/apis/options/options.go create mode 100644 pkg/apis/options/options_test.go diff --git a/Gopkg.lock b/Gopkg.lock index 89ba394..eae6ada 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -68,6 +68,14 @@ revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e" version = "v3.2.0" +[[projects]] + digest = "1:eb53021a8aa3f599d29c7102e65026242bdedce998a54837dc67f14b6a97c5fd" + name = "github.com/fsnotify/fsnotify" + packages = ["."] + pruneopts = "" + revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" + version = "v1.4.7" + [[projects]] digest = "1:8c7410dae63c74bd92db09bf33af7e0698b635ab6a397fd8e9e10dfcce3138ac" name = "github.com/go-redis/redis" @@ -103,6 +111,25 @@ revision = "9c11da706d9b7902c6da69c592f75637793fe121" version = "v2.0.0" +[[projects]] + digest = "1:d14365c51dd1d34d5c79833ec91413bfbb166be978724f15701e17080dc06dec" + name = "github.com/hashicorp/hcl" + packages = [ + ".", + "hcl/ast", + "hcl/parser", + "hcl/printer", + "hcl/scanner", + "hcl/strconv", + "hcl/token", + "json/parser", + "json/scanner", + "json/token", + ] + pruneopts = "" + revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241" + version = "v1.0.0" + [[projects]] digest = "1:b3c5b95e56c06f5aa72cb2500e6ee5f44fcd122872d4fec2023a488e561218bc" name = "github.com/hpcloud/tail" @@ -117,6 +144,14 @@ revision = "a30252cb686a21eb2d0b98132633053ec2f7f1e5" version = "v1.0.0" +[[projects]] + digest = "1:ae39921edb7f801f7ce1b6b5484f9715a1dd2b52cb645daef095cd10fd6ee774" + name = "github.com/magiconair/properties" + packages = ["."] + pruneopts = "" + revision = "de8848e004dd33dc07a2947b3d76f618a7fc7ef1" + version = "v1.8.1" + [[projects]] digest = "1:af67386ca553c04c6222f7b5b2f17bc97a5dfb3b81b706882c7fd8c72c30cf8f" name = "github.com/mbland/hmacauth" @@ -125,6 +160,14 @@ revision = "107c17adcc5eccc9935cd67d9bc2feaf5255d2cb" version = "1.0.2" +[[projects]] + digest = "1:bcc46a0fbd9e933087bef394871256b5c60269575bb661935874729c65bbbf60" + name = "github.com/mitchellh/mapstructure" + packages = ["."] + pruneopts = "" + revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe" + version = "v1.1.2" + [[projects]] branch = "master" digest = "1:15c0562bca5d78ac087fb39c211071dc124e79fb18f8b7c3f8a0bc7ffcb2a38e" @@ -181,6 +224,14 @@ revision = "90e289841c1ed79b7a598a7cd9959750cb5e89e2" version = "v1.5.0" +[[projects]] + digest = "1:3d2c33720d4255686b9f4a7e4d3b94938ee36063f14705c5eb0f73347ed4c496" + name = "github.com/pelletier/go-toml" + packages = ["."] + pruneopts = "" + revision = "728039f679cbcd4f6a54e080d2219a4c4928c546" + version = "v1.4.0" + [[projects]] digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411" name = "github.com/pmezard/go-difflib" @@ -200,6 +251,49 @@ pruneopts = "" revision = "0dec1b30a0215bb68605dfc568e8855066c9202d" +[[projects]] + digest = "1:956f655c87b7255c6b1ae6c203ebb0af98cf2a13ef2507e34c9bf1c0332ac0f5" + name = "github.com/spf13/afero" + packages = [ + ".", + "mem", + ] + pruneopts = "" + revision = "588a75ec4f32903aa5e39a2619ba6a4631e28424" + version = "v1.2.2" + +[[projects]] + digest = "1:ae3493c780092be9d576a1f746ab967293ec165e8473425631f06658b6212afc" + name = "github.com/spf13/cast" + packages = ["."] + pruneopts = "" + revision = "8c9545af88b134710ab1cd196795e7f2388358d7" + version = "v1.3.0" + +[[projects]] + digest = "1:cc15ae4fbdb02ce31f3392361a70ac041f4f02e0485de8ffac92bd8033e3d26e" + name = "github.com/spf13/jwalterweatherman" + packages = ["."] + pruneopts = "" + revision = "94f6ae3ed3bceceafa716478c5fbf8d29ca601a1" + version = "v1.1.0" + +[[projects]] + digest = "1:cbaf13cdbfef0e4734ed8a7504f57fe893d471d62a35b982bf6fb3f036449a66" + name = "github.com/spf13/pflag" + packages = ["."] + pruneopts = "" + revision = "298182f68c66c05229eb03ac171abe6e309ee79a" + version = "v1.0.3" + +[[projects]] + digest = "1:9b483544fcf4fc0155e566f49ee669c205fba12a36eb18e7f80510796a606db4" + name = "github.com/spf13/viper" + packages = ["."] + pruneopts = "" + revision = "b5bf975e5823809fb22c7644d008757f78a4259e" + version = "v1.4.0" + [[projects]] digest = "1:3926a4ec9a4ff1a072458451aa2d9b98acd059a45b38f7335d31e06c3d6a0159" name = "github.com/stretchr/testify" @@ -300,11 +394,14 @@ "internal/language", "internal/language/compact", "internal/tag", + "internal/triegen", + "internal/ucd", "internal/utf8internal", "language", "runes", "transform", "unicode/cldr", + "unicode/norm", ] pruneopts = "" revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475" @@ -409,6 +506,8 @@ "github.com/mreiferson/go-options", "github.com/onsi/ginkgo", "github.com/onsi/gomega", + "github.com/spf13/pflag", + "github.com/spf13/viper", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", "github.com/yhat/wsutil", diff --git a/pkg/apis/options/cookie.go b/pkg/apis/options/cookie.go index 80ecf57..6ee3982 100644 --- a/pkg/apis/options/cookie.go +++ b/pkg/apis/options/cookie.go @@ -1,15 +1,33 @@ package options -import "time" +import ( + "time" + + flag "github.com/spf13/pflag" +) // CookieOptions contains configuration options relating to Cookie configuration type CookieOptions struct { - CookieName string `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"` - CookieSecret string `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"` - CookieDomain string `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"` - CookiePath string `flag:"cookie-path" cfg:"cookie_path" env:"OAUTH2_PROXY_COOKIE_PATH"` - CookieExpire time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"` - CookieRefresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"` - CookieSecure bool `flag:"cookie-secure" cfg:"cookie_secure" env:"OAUTH2_PROXY_COOKIE_SECURE"` - CookieHTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly" env:"OAUTH2_PROXY_COOKIE_HTTPONLY"` + Name string `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME" default:"_oauth2_proxy"` + Secret string `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"` + Domain string `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"` + Path string `flag:"cookie-path" cfg:"cookie_path" env:"OAUTH2_PROXY_COOKIE_PATH" default:"/"` + Expire time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE" default:"604800000000000"` + Refresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH" default:"0"` + Secure bool `flag:"cookie-secure" cfg:"cookie_secure" env:"OAUTH2_PROXY_COOKIE_SECURE" default:"true"` + HTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly" env:"OAUTH2_PROXY_COOKIE_HTTPONLY" default:"true"` +} + +// cookieFlagSet contains flags related to Cookie configuration +var cookieFlagSet = flag.NewFlagSet("cookie", flag.ExitOnError) + +func init() { + cookieFlagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates") + cookieFlagSet.String("cookie-secret", "", "the seed string for secure cookies (optionally base64 encoded)") + cookieFlagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)*") + cookieFlagSet.String("cookie-path", "/", "an optional cookie path to force cookies to (ie: /poc/)*") + cookieFlagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie") + cookieFlagSet.Duration("cookie-refresh", time.Duration(0), "refresh the cookie after this duration; 0 to disable") + cookieFlagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag") + cookieFlagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag") } diff --git a/pkg/apis/options/default.go b/pkg/apis/options/default.go new file mode 100644 index 0000000..09551dd --- /dev/null +++ b/pkg/apis/options/default.go @@ -0,0 +1,68 @@ +package options + +import ( + "fmt" + "reflect" + "strconv" +) + +func defaultStruct(s interface{}) error { + val := reflect.ValueOf(s) + var typ reflect.Type + if val.Kind() == reflect.Ptr { + typ = val.Elem().Type() + } else { + typ = val.Type() + } + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + fieldV := reflect.Indirect(val).Field(i) + + // If the field is a ptr, recurse + if field.Type.Kind() == reflect.Ptr { + if fieldV.IsNil() { + fieldV.Set(reflect.New(fieldV.Type().Elem())) + } + err := defaultStruct(fieldV.Interface()) + if err != nil { + return fmt.Errorf("error defaulting %s: %v", field.Name, err) + } + continue + } + + // If the field is a ptr, recurse + if field.Type.Kind() == reflect.Struct { + newField := reflect.New(reflect.TypeOf(fieldV.Interface())) + err := defaultStruct(newField.Interface()) + if err != nil { + return fmt.Errorf("error defaulting %s: %v", field.Name, err) + } + fieldV.Set(newField.Elem()) + continue + } + + defaultVal := field.Tag.Get("default") + + if defaultVal != "" { + switch fieldV.Kind() { + case reflect.String: + fieldV.Set(reflect.ValueOf(defaultVal).Convert(field.Type)) + case reflect.Bool: + boolVal, err := strconv.ParseBool(defaultVal) + if err != nil { + return fmt.Errorf("expected default value (%s) to be bool: %v", defaultVal, err) + } + fieldV.Set(reflect.ValueOf(boolVal).Convert(field.Type)) + case reflect.Int64: + intVal, err := strconv.ParseInt(defaultVal, 10, 64) + if err != nil { + return fmt.Errorf("expected default value (%s) to be int: %v", defaultVal, err) + } + fieldV.Set(reflect.ValueOf(intVal).Convert(field.Type)) + } + + } + } + return nil +} diff --git a/pkg/apis/options/default_test.go b/pkg/apis/options/default_test.go new file mode 100644 index 0000000..efc1e9f --- /dev/null +++ b/pkg/apis/options/default_test.go @@ -0,0 +1,60 @@ +package options + +import ( + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("defaultStruct", func() { + type InnerTest struct { + TestString string `default:"bar"` + } + + type Test struct { + TestString string `default:"foo"` + TestBool bool `default:"true"` + TestDuration time.Duration `default:"60000000000"` + TestPtrStruct *InnerTest + TestStruct InnerTest + } + + var testStruct *Test + + BeforeEach(func() { + testStruct = &Test{} + err := defaultStruct(testStruct) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("with a string field", func() { + It("reads the correct default", func() { + Expect(testStruct.TestString).To(Equal("foo")) + }) + }) + + Context("with a bool field", func() { + It("reads the correct default", func() { + Expect(testStruct.TestBool).To(Equal(true)) + }) + }) + + Context("with a duration field", func() { + It("reads the correct default", func() { + Expect(testStruct.TestDuration).To(Equal(time.Minute)) + }) + }) + + Context("with a pointer struct field", func() { + It("defaults the values in the struct", func() { + Expect(testStruct.TestPtrStruct.TestString).To(Equal("bar")) + }) + }) + + Context("with a struct field", func() { + It("defaults the values in the struct", func() { + Expect(testStruct.TestStruct.TestString).To(Equal("bar")) + }) + }) +}) diff --git a/pkg/apis/options/options.go b/pkg/apis/options/options.go new file mode 100644 index 0000000..cc05343 --- /dev/null +++ b/pkg/apis/options/options.go @@ -0,0 +1,66 @@ +package options + +import ( + "fmt" + "io" + "strings" + + flag "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +// Options contains configuration options for the OAuth2 Proxy +type Options struct { + Cookie *CookieOptions +} + +// New creates a new deafulted copy of the Options struct +func New() *Options { + options := &Options{} + err := defaultStruct(options) + if err != nil { + // If we get an error here, there must be a code error + panic(err) + } + return options +} + +// Load reads a config file, flag arguments and the environment to set the +// correct configuration options +func Load(config io.Reader, configType string, args []string) (*Options, error) { + flagSet := flag.NewFlagSet("oauth2-proxy", flag.ExitOnError) + + // Add FlagSets to main flagSet + flagSet.AddFlagSet(cookieFlagSet) + + flagSet.Parse(args) + + // Create a viper for binding config + v := viper.New() + + // Bind flags to viper + err := v.BindPFlags(flagSet) + if err != nil { + return nil, err + } + + // Configure loading of environment variables + // All flag options are prefixed by the EnvPrefix + v.SetEnvPrefix("OAUTH2_PROXY_") + // Substitute "-" for "_" so `FOO_BAR` matches the flag `foo-bar` + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + v.AutomaticEnv() + + // Read the configuration file + if config != nil { + v.ReadConfig(config) + } + + // Unmarhsal the config into the Options struct + options := New() + err = v.Unmarshal(&options) + if err != nil { + return nil, fmt.Errorf("error unmarshalling config: %v", err) + } + return options, nil +} diff --git a/pkg/apis/options/options_test.go b/pkg/apis/options/options_test.go new file mode 100644 index 0000000..b14d60e --- /dev/null +++ b/pkg/apis/options/options_test.go @@ -0,0 +1,37 @@ +package options + +import ( + "io" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestOptions(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Options") +} + +var _ = Describe("Load", func() { + var opts *Options + var err error + var config io.Reader + var configType string + var args []string + + JustBeforeEach(func() { + opts, err = Load(config, configType, args) + }) + + Context("with no configuration", func() { + It("returns no error", func() { + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns the default configuration", func() { + defaultOpts := New() + Expect(opts).To(Equal(defaultOpts)) + }) + }) +})