Skip to content

Commit

Permalink
Allow validating config.Duration type (letsencrypt#7564)
Browse files Browse the repository at this point in the history
  • Loading branch information
pgporada authored Jun 27, 2024
1 parent 7d3c8af commit 0c04b0e
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 6 deletions.
7 changes: 7 additions & 0 deletions cmd/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
"google.golang.org/grpc/grpclog"

"github.com/letsencrypt/boulder/config"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/strictyaml"
Expand Down Expand Up @@ -455,6 +456,9 @@ func ValidateJSONConfig(cv *ConfigValidator, in io.Reader) error {
}
}

// Register custom types for use with existing validation tags.
validate.RegisterCustomTypeFunc(config.DurationCustomTypeFunc, config.Duration{})

err := decodeJSONStrict(in, cv.Config)
if err != nil {
return err
Expand Down Expand Up @@ -497,6 +501,9 @@ func ValidateYAMLConfig(cv *ConfigValidator, in io.Reader) error {
}
}

// Register custom types for use with existing validation tags.
validate.RegisterCustomTypeFunc(config.DurationCustomTypeFunc, config.Duration{})

inBytes, err := io.ReadAll(in)
if err != nil {
return err
Expand Down
40 changes: 36 additions & 4 deletions cmd/shell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import (
"testing"
"time"

"github.com/prometheus/client_golang/prometheus"

"github.com/letsencrypt/boulder/config"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/test"
"github.com/prometheus/client_golang/prometheus"
)

var (
Expand Down Expand Up @@ -196,9 +198,11 @@ func loadConfigFile(t *testing.T, path string) *os.File {

func TestFailedConfigValidation(t *testing.T) {
type FooConfig struct {
VitalValue string `yaml:"vitalValue" validate:"required"`
VoluntarilyVoid string `yaml:"voluntarilyVoid"`
VisciouslyVetted string `yaml:"visciouslyVetted" validate:"omitempty,endswith=baz"`
VitalValue string `yaml:"vitalValue" validate:"required"`
VoluntarilyVoid string `yaml:"voluntarilyVoid"`
VisciouslyVetted string `yaml:"visciouslyVetted" validate:"omitempty,endswith=baz"`
VolatileVagary config.Duration `yaml:"volatileVagary" validate:"required,lte=120s"`
VernalVeil config.Duration `yaml:"vernalVeil" validate:"required"`
}

// Violates 'endswith' tag JSON.
Expand Down Expand Up @@ -228,6 +232,34 @@ func TestFailedConfigValidation(t *testing.T) {
err = ValidateYAMLConfig(&ConfigValidator{&FooConfig{}, nil}, cf)
test.AssertError(t, err, "Expected validation error")
test.AssertContains(t, err.Error(), "'required'")

// Violates 'lte' tag JSON for config.Duration type.
cf = loadConfigFile(t, "testdata/3_configDuration_too_darn_big.json")
defer cf.Close()
err = ValidateJSONConfig(&ConfigValidator{&FooConfig{}, nil}, cf)
test.AssertError(t, err, "Expected validation error")
test.AssertContains(t, err.Error(), "'lte'")

// Violates 'lte' tag JSON for config.Duration type.
cf = loadConfigFile(t, "testdata/3_configDuration_too_darn_big.json")
defer cf.Close()
err = ValidateJSONConfig(&ConfigValidator{&FooConfig{}, nil}, cf)
test.AssertError(t, err, "Expected validation error")
test.AssertContains(t, err.Error(), "'lte'")

// Incorrect value for the config.Duration type.
cf = loadConfigFile(t, "testdata/4_incorrect_data_for_type.json")
defer cf.Close()
err = ValidateJSONConfig(&ConfigValidator{&FooConfig{}, nil}, cf)
test.AssertError(t, err, "Expected error")
test.AssertContains(t, err.Error(), "missing unit in duration")

// Incorrect value for the config.Duration type.
cf = loadConfigFile(t, "testdata/4_incorrect_data_for_type.yaml")
defer cf.Close()
err = ValidateYAMLConfig(&ConfigValidator{&FooConfig{}, nil}, cf)
test.AssertError(t, err, "Expected error")
test.AssertContains(t, err.Error(), "missing unit in duration")
}

func TestFailExit(t *testing.T) {
Expand Down
6 changes: 6 additions & 0 deletions cmd/testdata/3_configDuration_too_darn_big.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"vitalValue": "Gotcha",
"voluntarilyVoid": "Not used",
"visciouslyVetted": "Whateverbaz",
"volatileVagary": "121s"
}
7 changes: 7 additions & 0 deletions cmd/testdata/4_incorrect_data_for_type.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"vitalValue": "Gotcha",
"voluntarilyVoid": "Not used",
"visciouslyVetted": "Whateverbaz",
"volatileVagary": "120s",
"vernalVeil": "60"
}
5 changes: 5 additions & 0 deletions cmd/testdata/4_incorrect_data_for_type.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
vitalValue: "Gotcha"
voluntarilyVoid: "Not used"
visciouslyVetted: "Whateverbaz"
volatileVagary: "120s"
vernalVeil: "60"
16 changes: 14 additions & 2 deletions config/duration.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,27 @@ package config
import (
"encoding/json"
"errors"
"reflect"
"time"
)

// Duration is just an alias for time.Duration that allows
// serialization to YAML as well as JSON.
// Duration is custom type embedding a time.Duration which allows defining
// methods such as serialization to YAML or JSON.
type Duration struct {
time.Duration `validate:"required"`
}

// DurationCustomTypeFunc enables registration of our custom config.Duration
// type as a time.Duration and performing validation on the configured value
// using the standard suite of validation functions.
func DurationCustomTypeFunc(field reflect.Value) interface{} {
if c, ok := field.Interface().(Duration); ok {
return c.Duration
}

return reflect.Invalid
}

// ErrDurationMustBeString is returned when a non-string value is
// presented to be deserialized as a ConfigDuration
var ErrDurationMustBeString = errors.New("cannot JSON unmarshal something other than a string into a ConfigDuration")
Expand Down

0 comments on commit 0c04b0e

Please sign in to comment.