Skip to content

Commit

Permalink
Add []byte as base64 flag type and support direct binary from configm…
Browse files Browse the repository at this point in the history
…ap binaryData section for said type (#52)

* Add []byte as base64 flag type

* add error case

* fix the tostring to not be [ bytes...] but base64 too, add a binary to http example flags

* handle binary when reading from file

* add test for IsBinary() in same package as definition so coverage sees it's used

* also test for errors, add Errors() count
  • Loading branch information
ldemailly authored Oct 31, 2023
1 parent 12dd5e2 commit 4cfcd24
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 9 deletions.
36 changes: 33 additions & 3 deletions configmap/loglevel_change_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,26 @@ package configmap_test // this used to be in fortio.org/fortio/fnet/dyanmic_logg

import (
"flag"
"fmt"
"os"
"path"
"testing"
"time"

"fortio.org/assert"
"fortio.org/dflag"
"fortio.org/dflag/configmap"
"fortio.org/log"
)

func TestDynamicLogLevel(t *testing.T) {
func TestDynamicLogLevelAndBinaryFlag(t *testing.T) {
binF := dflag.Dyn(flag.CommandLine, "binary_flag", []byte{}, "a test binary flag").WithValidator(func(data []byte) error {
l := len(data)
if l > 4 {
return fmt.Errorf("generating error for binary flag len %d", l)
}
return nil
})
log.SetDefaultsForClientTools()
tmpDir, err := os.MkdirTemp("", "fortio-logger-test")
if err != nil {
Expand All @@ -40,6 +50,12 @@ func TestDynamicLogLevel(t *testing.T) {
if err = os.WriteFile(fName, []byte("ignored"), 0o644); err != nil {
t.Fatalf("unable to write %v: %v", fName, err)
}
binaryFlag := path.Join(pDir, "binary_flag")
if err = os.WriteFile(binaryFlag, []byte{0, 1, 2, 3}, 0o644); err != nil {
t.Fatalf("unable to write %v: %v", binaryFlag, err)
}
// Time based tests aren't great, specially when ran on (slow) CI try to have notification not get events for above.
time.Sleep(1 * time.Second)
var u *configmap.Updater
log.SetLogLevel(log.Debug)
if u, err = configmap.Setup(flag.CommandLine, pDir); err != nil {
Expand All @@ -49,16 +65,30 @@ func TestDynamicLogLevel(t *testing.T) {
if u.Warnings() != 1 {
t.Errorf("Expected exactly 1 warning (extra flag), got %d", u.Warnings())
}
assert.Equal(t, binF.Get(), []byte{0, 1, 2, 3})
// Now update that flag (and the loglevel)
if err = os.WriteFile(binaryFlag, []byte{1, 0}, 0o644); err != nil {
t.Fatalf("unable to write %v: %v", binaryFlag, err)
}
fName = path.Join(pDir, "loglevel")
// Test also the new normalization (space trimmimg and captitalization)
// Test also the new normalization (space trimming and capitalization)
if err = os.WriteFile(fName, []byte(" InFO\n\n"), 0o644); err != nil {
t.Fatalf("unable to write %v: %v", fName, err)
}
time.Sleep(1 * time.Second)
// Time based tests aren't great, specially when ran on (slow) CI but...
time.Sleep(2 * time.Second)
newLevel := log.GetLogLevel()
if newLevel != log.Info {
t.Errorf("Loglevel didn't change as expected, still %v %v", newLevel, newLevel.String())
}
assert.Equal(t, binF.Get(), []byte{1, 0})
// put back debug
log.SetLogLevel(log.Debug)
assert.Equal(t, u.Errors(), 0, "should have 0 errors so far")
// Now create validation error on binary flag:
if err = os.WriteFile(binaryFlag, []byte{1, 2, 3, 4, 5}, 0o644); err != nil {
t.Fatalf("unable to write %v: %v", binaryFlag, err)
}
time.Sleep(2 * time.Second)
assert.Equal(t, u.Errors(), 1, "should have 1 error picked up as we wrote > 4 bytes")
}
18 changes: 17 additions & 1 deletion configmap/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type Updater struct {
flagSet *flag.FlagSet
done chan bool
warnings atomic.Int32 // Count of unknown flags that have been logged (increases at each iteration).
errors atomic.Int32 // Count of validation errors that have been logged (increases at each iteration).
}

// Setup is a combination/shortcut for New+Initialize+Start.
Expand Down Expand Up @@ -133,6 +134,7 @@ func (u *Updater) readAll(dynamicOnly bool) error {
u.warnings.Add(1)
} else if !(errors.Is(err, errFlagNotDynamic) && dynamicOnly) {
errorStrings = append(errorStrings, fmt.Sprintf("flag %v: %v", f.Name(), err.Error()))
u.errors.Add(1)
}
}
}
Expand All @@ -148,6 +150,11 @@ func (u *Updater) Warnings() int {
return int(u.warnings.Load())
}

// Return the errors count.
func (u *Updater) Errors() int {
return int(u.errors.Load())
}

func (u *Updater) readFlagFile(fullPath string, dynamicOnly bool) error {
flagName := path.Base(fullPath)
flag := u.flagSet.Lookup(flagName)
Expand All @@ -161,8 +168,16 @@ func (u *Updater) readFlagFile(fullPath string, dynamicOnly bool) error {
if err != nil {
return err
}
if v := dflag.IsBinary(flag); v != nil {
log.Infof("Updating binary %q to new blob (len %d)", flagName, len(content))
err = v.SetV(content)
if err != nil {
return err
}
return nil
}
str := string(content)
log.Infof("updating %v to %q", flagName, str)
log.Infof("Updating %q to %q", flagName, str)
// do not call flag.Value.Set, instead go through flagSet.Set to change "changed" state.
return u.flagSet.Set(flagName, str)
}
Expand Down Expand Up @@ -193,6 +208,7 @@ func (u *Updater) watchForUpdates() {
flagName := path.Base(event.Name)
if err := u.readFlagFile(event.Name, true); err != nil {
log.Errf("dflag: failed setting flag %s: %v", flagName, err.Error())
u.errors.Add(1)
}
case fsnotify.Chmod:
}
Expand Down
18 changes: 16 additions & 2 deletions dyngeneric.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package dflag

import (
"encoding/base64"
"flag"
"fmt"
"strconv"
Expand Down Expand Up @@ -43,6 +44,15 @@ func IsFlagDynamic(f *flag.Flag) bool {
return df.IsDynamicFlag() // will clearly return true if it exists
}

// IsBinary returns the binary flag or nil depending on if the given Flag
// is a []byte dynamic value or not (for confimap/file based setting).
func IsBinary(f *flag.Flag) *DynValue[[]byte] {
if v, ok := f.Value.(*DynValue[[]byte]); ok {
return v
}
return nil
}

type DynamicBoolValueTag struct{}

func (*DynamicBoolValueTag) IsBoolFlag() bool {
Expand Down Expand Up @@ -74,7 +84,7 @@ func ValidateDynSliceMinElements[T any](count int) func([]T) error {
// DynValueTypes are the types currently supported by Parse[T] and thus by Dyn[T].
// DynJSON is special.
type DynValueTypes interface {
bool | time.Duration | float64 | int64 | string | []string | sets.Set[string]
bool | time.Duration | float64 | int64 | string | []string | sets.Set[string] | []byte
}

type DynValue[T any] struct {
Expand Down Expand Up @@ -189,6 +199,8 @@ func parse[T any](input string) (val T, err error) {
*v, err = strconv.ParseFloat(strings.TrimSpace(input), 64)
case *time.Duration:
*v, err = time.ParseDuration(input)
case *[]byte:
*v, err = base64.StdEncoding.DecodeString(input)
case *string:
*v = input
case *[]string:
Expand Down Expand Up @@ -275,8 +287,10 @@ func (d *DynValue[T]) String() string {
switch v := any(d.Get()).(type) {
case []string:
return strings.Join(v, ",")
case []byte:
return base64.StdEncoding.EncodeToString(v)
default:
return fmt.Sprintf("%v", d.Get())
return fmt.Sprintf("%v", v)
}
}

Expand Down
25 changes: 24 additions & 1 deletion dyngeneric_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,16 @@ func TestArrayToString(t *testing.T) {
s := []string{"z", "a", "c", "b"}
f := New(s, "test array")
Flag("testing123", f)
defValue := flag.CommandLine.Lookup("testing123").DefValue
flag := flag.CommandLine.Lookup("testing123")
defValue := flag.DefValue
// order preserved unlike for sets.Set where we sort
str := f.String()
assert.Equal(t, "z,a,c,b", str)
assert.Equal(t, "z,a,c,b", defValue)
b := IsBinary(flag)
if b != nil {
t.Errorf("flag %v isn't binary yet got non nil: %v", flag, b)
}
}

func TestRemoveCommon(t *testing.T) {
Expand All @@ -70,3 +75,21 @@ func TestRemoveCommon(t *testing.T) {
setBB.Remove("c")
assert.False(t, setBB.Has("c"))
}

func TestBinary(t *testing.T) {
set := flag.NewFlagSet("foobar", flag.ContinueOnError)
dynFlag := Dyn(set, "some_binary", []byte{2, 1, 0}, "some binary values")
assert.Equal(t, []byte{2, 1, 0}, dynFlag.Get(), "value must be default after create")
err := set.Set("some_binary", "\nAAEC\n") // extra newlines are fine
assert.NoError(t, err, "setting value must succeed")
assert.Equal(t, []byte{0, 1, 2}, dynFlag.Get(), "value must be set after update")
str := dynFlag.String()
assert.Equal(t, "AAEC", str, "value when printed must be base64 encoded")
err = set.Set("some_binary", "foo bar")
assert.Error(t, err, "setting bogus base64 should fail")
flag := set.Lookup("some_binary")
assert.True(t, IsFlagDynamic(flag), "flag must be dynamic")
if IsBinary(flag) == nil {
t.Errorf("flag %v isn't binary yet it should", flag)
}
}
5 changes: 3 additions & 2 deletions examples/server_kube/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ var (
},
},
"An arbitrary JSON struct.")
dynArray = dflag.New([]string{"z", "b", "a"}, "An array of strings (comma separated)")
dynSet = dflag.New(sets.New("z", "b", "a"), "An set of strings (comma separated)")
dynArray = dflag.New([]string{"z", "b", "a"}, "An array of strings (comma separated)")
dynSet = dflag.New(sets.New("z", "b", "a"), "An set of strings (comma separated)")
dynBinary = dflag.Dyn(flag.CommandLine, "example_binary", []byte{0x00, 0x01, 0x02}, "A binary value")
)

func main() {
Expand Down

0 comments on commit 4cfcd24

Please sign in to comment.