Skip to content
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

Add []byte as base64 flag type and support direct binary from configmap binaryData section for said type #52

Merged
merged 7 commits into from
Oct 31, 2023
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