structured flags as alternative to config #76

This commit is contained in:
Umputun
2018-06-10 02:27:37 -05:00
parent 864cdf3a1d
commit c3cd7877ef
19 changed files with 181 additions and 82 deletions
Generated
+3 -3
View File
@@ -115,10 +115,10 @@
revision = "0dc08b1671f34c4250ce212759ebd880f743d883"
[[projects]]
branch = "master"
name = "github.com/jessevdk/go-flags"
packages = ["."]
revision = "96dc06278ce32a0e9d957d590bb987c81ee66407"
version = "v1.3.0"
revision = "1c38ed7ad0cc3d9e66649ac398c30e45f395c4eb"
[[projects]]
branch = "master"
@@ -223,6 +223,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "e849348430ea10a86642c328c52b516e237207389cf765ff1849c8bded391d4a"
inputs-digest = "2c90909156f93c975489ffa0340d12a2f9ce4369166c93bbfc531fdbbf40efb6"
solver-name = "gps-cdcl"
solver-version = 1
+4 -1
View File
@@ -30,4 +30,7 @@ required = ["github.com/patrickmn/go-cache"]
[[constraint]]
name = "github.com/patrickmn/go-cache"
version = "2.1.0"
version = "2.1.0"
[[constraint]]
branch = "master"
name = "github.com/jessevdk/go-flags"
+30 -30
View File
@@ -30,34 +30,34 @@ Remark42 is a self-hosted, lightweight, and simple (yet functional) comment engi
#### Parameters
| Command line | Environment | Default | Multi | Description |
| ----------------- | -------------------- | ---------------------- | ----- | ---------------------------------------------- |
| --url | REMARK_URL | | no | url to remark42 server |
| --bolt | BOLTDB_PATH | `./var` | no | path to data directory |
| --site | SITE | `remark` | yes | site name(s) |
| --admin | ADMIN | | yes | admin names (list of user ids) |
| --admin-email | ADMIN_EMAIL | `admin@${REMARK_URL}` | no | admin email |
| --backup | BACKUP_PATH | `./var/backup` | no | backups location |
| --max-back | MAX_BACKUP_FILES | `10` | no | max backup files to keep |
| --max-cache-items | MAX_CACHE_ITEMS | `1000` | no | max number of cached items, `0` - unlimited |
| --max-cache-value | MAX_CACHE_VALUE | `65536` | no | max size of cached value, `0` - unlimited |
| --max-cache-size | MAX_CACHE_SIZE | `50000000` | no | max size of all cached values, `0` - unlimited |
| --avatars | AVATAR_STORE | `./var/avatars` | no | avatars location |
| --secret | SECRET | | no | secret key, required |
| --max-comment | MAX_COMMENT_SIZE | 2048 | no | comment's size limit |
| --google-cid | REMARK_GOOGLE_CID | | no | Google OAuth client ID |
| --google-csec | REMARK_GOOGLE_CSEC | | no | Google OAuth client secret |
| --facebook-cid | REMARK_FACEBOOK_CID | | no | Facebook OAuth client ID |
| --facebook-csec | REMARK_FACEBOOK_CSEC | | no | Facebook OAuth client secret |
| --github-cid | REMARK_GITHUB_CID | | no | Github OAuth client ID |
| --github-csec | REMARK_GITHUB_CSEC | | no | Github OAuth client secret |
| --yandex-cid | REMARK_YANDEX_CID | | no | Yandex OAuth client ID |
| --yandex-csec | REMARK_YANDEX_CSEC | | no | Yandex OAuth client secret |
| --low-score | LOW_SCORE | `-5` | no | Low score threshold |
| --critical-score | CRITICAL_SCORE | `-10` | no | Critical score threshold |
| --img-proxy | IMG_PROXY | `false` | no | Enable http->https proxy for images |
| --dbg | DEBUG | `false` | no | debug mode |
| --dev-passwd | DEV_PASSWD | | no | password for `dev` user |
| Command line | Environment | Default | Multi | Description |
| -------------------- | ------------------ | --------------------- | ----- | ---------------------------------------------- |
| --url | REMARK_URL | | no | url to remark42 server, required |
| --secret | SECRET | | no | secret key, required |
| --bolt | BOLTDB_PATH | `./var` | no | path to data directory |
| --site | SITE | `remark` | yes | site name(s) |
| --admin | ADMIN | | yes | admin names (list of user ids) |
| --admin-email | ADMIN_EMAIL | `admin@${REMARK_URL}` | no | admin email | |
| --backup | BACKUP_PATH | `./var/backup` | no | backups location |
| --max-back | MAX_BACKUP_FILES | `10` | no | max backup files to keep |
| --max-cache-items | MAX_CACHE_ITEMS | `1000` | no | max number of cached items, `0` - unlimited |
| --max-cache-value | MAX_CACHE_VALUE | `65536` | no | max size of cached value, `0` - unlimited |
| --max-cache-size | MAX_CACHE_SIZE | `50000000` | no | max size of all cached values, `0` - unlimited |
| --avatars | AVATAR_STORE | `./var/avatars` | no | avatars location |
| --max-comment | MAX_COMMENT_SIZE | 2048 | no | comment's size limit |
| --auth.google.cid | AUTH_GOOGLE_CID | | no | Google OAuth client ID |
| --auth.google.csec | AUTH_GOOGLE_CSEC | | no | Google OAuth client secret |
| --auth.facebook.cid | AUTH_FACEBOOK_CID | | no | Facebook OAuth client ID |
| --auth.facebook.csec | AUTH_FACEBOOK_CSEC | | no | Facebook OAuth client secret |
| --auth.github.cid | AUTH_GITHUB_CID | | no | Github OAuth client ID |
| --auth.github.csec | AUTH_GITHUB_CSEC | | no | Github OAuth client secret |
| --auth.yandex.cid | AUTH_YANDEX_CID | | no | Yandex OAuth client ID |
| --auth.yandex.csec | AUTH_YANDEX_CSEC | | no | Yandex OAuth client secret |
| --low-score | LOW_SCORE | `-5` | no | Low score threshold |
| --critical-score | CRITICAL_SCORE | `-10` | no | Critical score threshold |
| --img-proxy | IMG_PROXY | `false` | no | Enable http->https proxy for images |
| --dbg | DEBUG | `false` | no | debug mode |
| --dev-passwd | DEV_PASSWD | | no | password for `dev` user |
##### Required parameters
@@ -81,8 +81,8 @@ services:
- REMARK_URL=https://demo.remark42.com # url pointing to your remark42 server
- USER=1001 # UID on the host machine
- SECRET=abcd-123456-xyz-$%^& # secret key
- REMARK_GITHUB_CID=12345667890 # oauth2 client ID
- REMARK_GITHUB_CSEC=abcdefg12345678 # oauth2 client secret
- AUTH_GITHUB_CID=12345667890 # oauth2 client ID
- AUTH_GITHUB_CSEC=abcdefg12345678 # oauth2 client secret
volumes:
- ./var:/srv/var # persistent volume to store all remark42 data
```
+28 -18
View File
@@ -52,17 +52,21 @@ type Opts struct {
CriticalScore int `long:"critical-score" env:"CRITICAL_SCORE" default:"-10" description:"critical score threshold"`
ReadOnlyAge int `long:"read-age" env:"READONLY_AGE" default:"0" description:"read-only age of comments"`
GoogleCID string `long:"google-cid" env:"REMARK_GOOGLE_CID" description:"Google OAuth client ID"`
GoogleCSEC string `long:"google-csec" env:"REMARK_GOOGLE_CSEC" description:"Google OAuth client secret"`
GithubCID string `long:"github-cid" env:"REMARK_GITHUB_CID" description:"Github OAuth client ID"`
GithubCSEC string `long:"github-csec" env:"REMARK_GITHUB_CSEC" description:"Github OAuth client secret"`
FacebookCID string `long:"facebook-cid" env:"REMARK_FACEBOOK_CID" description:"Facebook OAuth client ID"`
FacebookCSEC string `long:"facebook-csec" env:"REMARK_FACEBOOK_CSEC" description:"Facebook OAuth client secret"`
YandexCID string `long:"yandex-cid" env:"REMARK_YANDEX_CID" description:"Yandex OAuth client ID"`
YandexCSEC string `long:"yandex-csec" env:"REMARK_YANDEX_CSEC" description:"Yandex OAuth client secret"`
Port int `long:"port" env:"REMARK_PORT" default:"8080" description:"port"`
WebRoot string `long:"web-root" env:"REMARK_WEB_ROOT" default:"./web" description:"web root directory"`
Auth struct {
Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"`
Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"`
Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"`
Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"`
} `group:"auth" namespace:"auth" env-namespace:"AUTH"`
}
// AuthGroup defines options group for auth params
type AuthGroup struct {
CID string `long:"cid" env:"CID" description:"OAuth client ID"`
CSEC string `long:"csec" env:"CSEC" description:"OAuth client secret"`
}
var revision = "unknown"
@@ -85,7 +89,7 @@ func main() {
os.Exit(1)
}
log.Print("[INFO] started remark")
resetEnv("SECRET", "AUTH_GOOGLE_CSEC", "AUTH_GITHUB_CSEC", "AUTH_FACEBOOK_CSEC", "AUTH_YANDEX_CSEC")
ctx, cancel := context.WithCancel(context.Background())
go func() { // catch signal and invoke graceful termination
stop := make(chan os.Signal, 1)
@@ -279,17 +283,17 @@ func makeAuthProviders(jwtService *auth.JWT, avatarProxy *proxy.Avatar, ds *serv
}
providers := []auth.Provider{}
if opts.GoogleCID != "" && opts.GoogleCSEC != "" {
providers = append(providers, auth.NewGoogle(makeParams(opts.GoogleCID, opts.GoogleCSEC)))
if opts.Auth.Google.CID != "" && opts.Auth.Google.CSEC != "" {
providers = append(providers, auth.NewGoogle(makeParams(opts.Auth.Google.CID, opts.Auth.Google.CSEC)))
}
if opts.GithubCID != "" && opts.GithubCSEC != "" {
providers = append(providers, auth.NewGithub(makeParams(opts.GithubCID, opts.GithubCSEC)))
if opts.Auth.Github.CID != "" && opts.Auth.Github.CSEC != "" {
providers = append(providers, auth.NewGithub(makeParams(opts.Auth.Github.CID, opts.Auth.Github.CSEC)))
}
if opts.FacebookCID != "" && opts.FacebookCSEC != "" {
providers = append(providers, auth.NewFacebook(makeParams(opts.FacebookCID, opts.FacebookCSEC)))
if opts.Auth.Facebook.CID != "" && opts.Auth.Facebook.CSEC != "" {
providers = append(providers, auth.NewFacebook(makeParams(opts.Auth.Facebook.CID, opts.Auth.Facebook.CSEC)))
}
if opts.YandexCID != "" && opts.YandexCSEC != "" {
providers = append(providers, auth.NewYandex(makeParams(opts.YandexCID, opts.YandexCSEC)))
if opts.Auth.Yandex.CID != "" && opts.Auth.Yandex.CSEC != "" {
providers = append(providers, auth.NewYandex(makeParams(opts.Auth.Yandex.CID, opts.Auth.Yandex.CSEC)))
}
if len(providers) == 0 {
log.Printf("[WARN] no auth providers defined")
@@ -322,6 +326,12 @@ func postFlushFn(sites []string, port int) func() {
}
}
func resetEnv(envs ...string) {
for _, env := range envs {
os.Unsetenv(env)
}
}
func setupLog(dbg bool) {
filter := &logutils.LevelFilter{
Levels: []logutils.LogLevel{"DEBUG", "INFO", "WARN", "ERROR"},
+4 -4
View File
@@ -98,10 +98,10 @@ func prepApp(t *testing.T, port int, duration time.Duration) (*Application, cont
p.ParseArgs([]string{"--secret=123456", "--dev-passwd=password", "--url=https://demo.remark42.com"})
opts.AvatarStore, opts.BackupLocation = "/tmp", "/tmp"
opts.BoltPath = fmt.Sprintf("/tmp/%d", port)
opts.GithubCSEC, opts.GithubCID = "csec", "cid"
opts.GoogleCSEC, opts.GoogleCID = "csec", "cid"
opts.FacebookCSEC, opts.FacebookCID = "csec", "cid"
opts.YandexCSEC, opts.YandexCID = "csec", "cid"
opts.Auth.Github.CSEC, opts.Auth.Github.CID = "csec", "cid"
opts.Auth.Google.CSEC, opts.Auth.Google.CID = "csec", "cid"
opts.Auth.Facebook.CSEC, opts.Auth.Facebook.CID = "csec", "cid"
opts.Auth.Yandex.CSEC, opts.Auth.Yandex.CID = "csec", "cid"
opts.Port = port
os.Remove(opts.BoltPath + "/remark.db")
+9 -3
View File
@@ -1,8 +1,15 @@
language: go
os:
- linux
- osx
go:
- 1.6.x
- 1.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
install:
# go-flags
@@ -10,8 +17,7 @@ install:
- go build -v ./...
# linting
- go get github.com/golang/lint
- go install github.com/golang/lint/golint
- go get github.com/golang/lint/golint
# code coverage
- go get golang.org/x/tools/cmd/cover
-1
View File
@@ -110,7 +110,6 @@ args, err := flags.ParseArgs(&opts, args)
if err != nil {
panic(err)
os.Exit(1)
}
fmt.Printf("Verbosity: %v\n", opts.Verbose)
+12 -2
View File
@@ -5,7 +5,6 @@ import (
"sort"
"strconv"
"strings"
"unsafe"
)
// Command represents an application command. Commands can be added to the
@@ -229,7 +228,17 @@ func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler {
subcommand := mtag.Get("command")
if len(subcommand) != 0 {
ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr()))
var ptrval reflect.Value
if realval.Kind() == reflect.Ptr {
ptrval = realval
if ptrval.IsNil() {
ptrval.Set(reflect.New(ptrval.Type().Elem()))
}
} else {
ptrval = realval.Addr()
}
shortDescription := mtag.Get("description")
longDescription := mtag.Get("long-description")
@@ -237,6 +246,7 @@ func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler {
aliases := mtag.GetMany("alias")
subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface())
if err != nil {
return true, err
}
+4
View File
@@ -125,6 +125,10 @@ The following is a list of tags for struct fields supported by go-flags:
gets prepended to every option's long name and
subgroup's namespace of this group, separated by
the parser's namespace delimiter (optional)
env-namespace: when specified on a group struct field, the env-namespace
gets prepended to every option's env key and
subgroup's env-namespace of this group, separated by
the parser's env-namespace delimiter (optional)
command: when specified on a struct field, makes the struct
field a (sub)command with the given name (optional)
subcommands-optional: when specified on a command struct field, makes
+17 -2
View File
@@ -9,7 +9,6 @@ import (
"reflect"
"strings"
"unicode/utf8"
"unsafe"
)
// ErrNotPointerToStruct indicates that a provided data container is not
@@ -35,6 +34,9 @@ type Group struct {
// The namespace of the group
Namespace string
// The environment namespace of the group
EnvNamespace string
// If true, the group is not displayed in the help or man page
Hidden bool
@@ -338,15 +340,28 @@ func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.Struc
subgroup := mtag.Get("group")
if len(subgroup) != 0 {
ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr()))
var ptrval reflect.Value
if realval.Kind() == reflect.Ptr {
ptrval = realval
if ptrval.IsNil() {
ptrval.Set(reflect.New(ptrval.Type()))
}
} else {
ptrval = realval.Addr()
}
description := mtag.Get("description")
group, err := g.AddGroup(subgroup, description, ptrval.Interface())
if err != nil {
return true, err
}
group.Namespace = mtag.Get("namespace")
group.EnvNamespace = mtag.Get("env-namespace")
group.Hidden = mtag.Get("hidden") != ""
return true, nil
+3 -3
View File
@@ -225,12 +225,12 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig
}
var envDef string
if option.EnvDefaultKey != "" {
if option.EnvKeyWithNamespace() != "" {
var envPrintable string
if runtime.GOOS == "windows" {
envPrintable = "%" + option.EnvDefaultKey + "%"
envPrintable = "%" + option.EnvKeyWithNamespace() + "%"
} else {
envPrintable = "$" + option.EnvDefaultKey
envPrintable = "$" + option.EnvKeyWithNamespace()
}
envDef = fmt.Sprintf(" [%s]", envPrintable)
}
+3 -3
View File
@@ -83,11 +83,11 @@ func writeManPageOptions(wr io.Writer, grp *Group) {
if len(opt.Default) != 0 {
fmt.Fprintf(wr, " <default: \\fI%s\\fR>", manQuote(strings.Join(quoteV(opt.Default), ", ")))
} else if len(opt.EnvDefaultKey) != 0 {
} else if len(opt.EnvKeyWithNamespace()) != 0 {
if runtime.GOOS == "windows" {
fmt.Fprintf(wr, " <default: \\fI%%%s%%\\fR>", manQuote(opt.EnvDefaultKey))
fmt.Fprintf(wr, " <default: \\fI%%%s%%\\fR>", manQuote(opt.EnvKeyWithNamespace()))
} else {
fmt.Fprintf(wr, " <default: \\fI$%s\\fR>", manQuote(opt.EnvDefaultKey))
fmt.Fprintf(wr, " <default: \\fI$%s\\fR>", manQuote(opt.EnvKeyWithNamespace()))
}
}
+55 -7
View File
@@ -3,9 +3,9 @@ package flags
import (
"bytes"
"fmt"
"os"
"reflect"
"strings"
"syscall"
"unicode/utf8"
)
@@ -139,6 +139,57 @@ func (option *Option) LongNameWithNamespace() string {
return longName
}
// EnvKeyWithNamespace returns the option's env key with the group namespaces
// prepended by walking up the option's group tree. Namespaces and the env key
// itself are separated by the parser's namespace delimiter. If the env key is
// empty an empty string is returned.
func (option *Option) EnvKeyWithNamespace() string {
if len(option.EnvDefaultKey) == 0 {
return ""
}
// fetch the namespace delimiter from the parser which is always at the
// end of the group hierarchy
namespaceDelimiter := ""
g := option.group
for {
if p, ok := g.parent.(*Parser); ok {
namespaceDelimiter = p.EnvNamespaceDelimiter
break
}
switch i := g.parent.(type) {
case *Command:
g = i.Group
case *Group:
g = i
}
}
// concatenate long name with namespace
key := option.EnvDefaultKey
g = option.group
for g != nil {
if g.EnvNamespace != "" {
key = g.EnvNamespace + namespaceDelimiter + key
}
switch i := g.parent.(type) {
case *Command:
g = i.Group
case *Group:
g = i
case *Parser:
g = nil
}
}
return key
}
// String converts an option to a human friendly readable string describing the
// option.
func (option *Option) String() string {
@@ -260,13 +311,10 @@ func (option *Option) empty() {
func (option *Option) clearDefault() {
usedDefault := option.Default
if envKey := option.EnvDefaultKey; envKey != "" {
// os.Getenv() makes no distinction between undefined and
// empty values, so we use syscall.Getenv()
if value, ok := syscall.Getenv(envKey); ok {
if envKey := option.EnvKeyWithNamespace(); envKey != "" {
if value, ok := os.LookupEnv(envKey); ok {
if option.EnvDefaultDelim != "" {
usedDefault = strings.Split(value,
option.EnvDefaultDelim)
usedDefault = strings.Split(value, option.EnvDefaultDelim)
} else {
usedDefault = []string{value}
}
+7 -3
View File
@@ -29,6 +29,9 @@ type Parser struct {
// NamespaceDelimiter separates group namespaces and option long names
NamespaceDelimiter string
// EnvNamespaceDelimiter separates group env namespaces and env keys
EnvNamespaceDelimiter string
// UnknownOptionsHandler is a function which gets called when the parser
// encounters an unknown option. The function receives the unknown option
// name, a SplitArgument which specifies its value if set with an argument
@@ -170,9 +173,10 @@ func NewParser(data interface{}, options Options) *Parser {
// be added to this parser by using AddGroup and AddCommand.
func NewNamedParser(appname string, options Options) *Parser {
p := &Parser{
Command: newCommand(appname, "", "", nil),
Options: options,
NamespaceDelimiter: ".",
Command: newCommand(appname, "", "", nil),
Options: options,
NamespaceDelimiter: ".",
EnvNamespaceDelimiter: "_",
}
p.Command.parent = p
+1 -1
View File
@@ -1,4 +1,4 @@
// +build !windows,!plan9,!solaris
// +build !windows,!plan9,!solaris,!appengine
package flags
+1 -1
View File
@@ -1,4 +1,4 @@
// +build windows plan9 solaris
// +build windows plan9 solaris appengine
package flags