Skip to content

Commit

Permalink
Tools 2715 add the validate command (#23)
Browse files Browse the repository at this point in the history
* feat: tools2715 add the validate command

* chore: reformat validation errors

* ci: validation integration tests
  • Loading branch information
dwelch-spike authored Nov 14, 2023
1 parent 17ed370 commit f7e7e88
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 21 deletions.
83 changes: 75 additions & 8 deletions asconf/asconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package asconf

import (
"bytes"
"errors"
"fmt"
"sort"
"strings"

"github.com/aerospike/aerospike-management-lib/asconfig"
"github.com/go-logr/logr"
Expand Down Expand Up @@ -59,21 +62,85 @@ func NewAsconf(source []byte, srcFmt, outFmt Format, aerospikeVersion string, lo
return ac, err
}

func (ac *asconf) Validate() error {
type ValidationErr struct {
asconfig.ValidationErr
}

type VErrSlice []ValidationErr

func (a VErrSlice) Len() int { return len(a) }
func (a VErrSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a VErrSlice) Less(i, j int) bool { return strings.Compare(a[i].Error(), a[j].Error()) == -1 }

func (o ValidationErr) Error() string {
verrTemplate := "description: %s, error-type: %s"
return fmt.Sprintf(verrTemplate, o.Description, o.ErrType)
}

type ValidationErrors struct {
Errors VErrSlice
}

func (o ValidationErrors) Error() string {
errorsByContext := map[string]VErrSlice{}

sort.Sort(o.Errors)

for _, err := range o.Errors {
errorsByContext[err.Context] = append(errorsByContext[err.Context], err)
}

contexts := []string{}
for ctx := range errorsByContext {
contexts = append(contexts, ctx)
}

sort.Strings(contexts)

errString := ""

for _, ctx := range contexts {
errString += fmt.Sprintf("context: %s\n", ctx)

errList := errorsByContext[ctx]
for _, err := range errList {

// filter "Must validate one and only one schema " errors
// I have never seen a useful one and they seem to always be
// accompanied by another more useful error that will be displayed
if err.ErrType == "number_one_of" {
continue
}

errString += fmt.Sprintf("\t- %s\n", err.Error())
}
}

return errString
}

// Validate validates the parsed configuration in ac against
// the Aerospike schema matching ac.aerospikeVersion.
// ValidationErrors is not nil if any errors during validation occur.
// ValidationErrors Error() method outputs a human readable string of validation error details.
// error is not nil if validation, or any other type of error occurs.
func (ac *asconf) Validate() (*ValidationErrors, error) {

valid, validationErrors, err := ac.cfg.IsValid(ac.managementLibLogger, ac.aerospikeVersion)
valid, tempVerrs, err := ac.cfg.IsValid(ac.managementLibLogger, ac.aerospikeVersion)

if len(validationErrors) > 0 {
for _, e := range validationErrors {
ac.logger.Errorf("Aerospike config validation error: %+v", e)
verrs := ValidationErrors{}
for _, v := range tempVerrs {
verr := ValidationErr{
ValidationErr: *v,
}
verrs.Errors = append(verrs.Errors, verr)
}

if !valid || err != nil || len(validationErrors) > 0 {
return fmt.Errorf("%w, %w", ErrConfigValidation, err)
if !valid || err != nil || len(verrs.Errors) > 0 {
return &verrs, errors.Join(ErrConfigValidation, err)
}

return err
return nil, nil
}

func (ac *asconf) MarshalText() (text []byte, err error) {
Expand Down
2 changes: 1 addition & 1 deletion asconf/asconf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func Test_asconf_Validate(t *testing.T) {
src: tt.fields.src,
aerospikeVersion: tt.fields.aerospikeVersion,
}
if err := ac.Validate(); (err != nil) != tt.wantErr {
if _, err := ac.Validate(); (err != nil) != tt.wantErr {
t.Errorf("asconf.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
Expand Down
11 changes: 6 additions & 5 deletions cmd/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var convertCmd = newConvertCmd()

func newConvertCmd() *cobra.Command {
res := &cobra.Command{
Use: "convert [flags] <path/to/config.yaml>",
Use: "convert [flags] <path/to/config_file>",
Short: "Convert between yaml and Aerospike config format.",
Long: `Convert is used to convert between yaml and aerospike configuration
files. Input files are converted to their opposite format, yaml -> conf, conf -> yaml.
Expand Down Expand Up @@ -115,9 +115,9 @@ func newConvertCmd() *cobra.Command {
}

if !force {
err = conf.Validate()
if err != nil {
return err
verrs, err := conf.Validate()
if err != nil || verrs != nil {
return errors.Join(err, verrs)
}
}

Expand Down Expand Up @@ -222,7 +222,8 @@ func newConvertCmd() *cobra.Command {

// flags and configuration settings
// aerospike-version is marked required in this cmd's PreRun if the --force flag is not provided
res.Flags().StringP("aerospike-version", "a", "", "Aerospike server version to validate the configuration file for. Ex: 6.2.0.\nThe first 3 digits of the Aerospike version number are required.\nThis option is required unless --force is used")
commonFlags := getCommonFlags()
res.Flags().AddFlagSet(commonFlags)
res.Flags().BoolP("force", "f", false, "Override checks for supported server version and config validation")
res.Flags().StringP("output", "o", os.Stdout.Name(), "File path to write output to")

Expand Down
4 changes: 2 additions & 2 deletions cmd/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type runTestDiff struct {
expectError bool
}

var testArgs = []runTestDiff{
var testDiffArgs = []runTestDiff{
{
flags: []string{},
arguments: []string{"not_enough_args"},
Expand Down Expand Up @@ -64,7 +64,7 @@ var testArgs = []runTestDiff{
func TestRunEDiff(t *testing.T) {
cmd := diffCmd

for i, test := range testArgs {
for i, test := range testDiffArgs {
cmd.ParseFlags(test.flags)
err := cmd.RunE(cmd, test.arguments)
if test.expectError == (err == nil) {
Expand Down
9 changes: 9 additions & 0 deletions cmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@ import (
"github.com/aerospike/asconfig/asconf"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

// common flags
func getCommonFlags() *pflag.FlagSet {
res := &pflag.FlagSet{}
res.StringP("aerospike-version", "a", "", "Aerospike server version to validate the configuration file for. Ex: 6.2.0.\nThe first 3 digits of the Aerospike version number are required.\nThis option is required unless --force is used.")

return res
}

// getConfFileFormat guesses the format of an input config file
// based on file extension and the --format flag of the cobra command
// this function implements the defaults scheme for file formats in asconfig
Expand Down
108 changes: 108 additions & 0 deletions cmd/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package cmd

import (
"errors"
"fmt"
"os"

"github.com/aerospike/asconfig/asconf"
"github.com/spf13/cobra"
)

const (
validateArgMax = 1
)

var (
errValidateTooManyArguments = fmt.Errorf("expected a maximum of %d arguments", convertArgMax)
)

func init() {
rootCmd.AddCommand(validateCmd)
}

var validateCmd = newValidateCmd()

func newValidateCmd() *cobra.Command {
res := &cobra.Command{
Use: "validate [flags] <path/to/config_file>",
Short: "Validate an Aerospike configuration file.",
Long: `Validate an Aerospike configuration file in any supported format
against a versioned Aerospike configuration schema.
If a file passes validation nothing is output, otherwise errors
indicating problems with the configuration file are shown.
If a file path is not provided, validate reads from stdin.
Ex: asconfig validate --aerospike-version 7.0.0 aerospike.conf`,
RunE: func(cmd *cobra.Command, args []string) error {
logger.Debug("Running validate command")

if len(args) > validateArgMax {
return errValidateTooManyArguments
}

// read stdin by default
var srcPath string
if len(args) == 0 {
srcPath = os.Stdin.Name()
} else {
srcPath = args[0]
}

version, err := cmd.Flags().GetString("aerospike-version")
if err != nil {
return err
}

logger.Debugf("Processing flag aerospike-version value=%s", version)

srcFormat, err := getConfFileFormat(srcPath, cmd)
if err != nil {
return err
}

logger.Debugf("Processing flag format value=%v", srcFormat)

fdata, err := os.ReadFile(srcPath)
if err != nil {
return err
}

conf, err := asconf.NewAsconf(
fdata,
srcFormat,
// we aren't converting to anything so set
// output format to Invalid as a place holder
asconf.Invalid,
version,
logger,
managementLibLogger,
)

if err != nil {
return err
}

verrs, err := conf.Validate()
if verrs != nil {
// force validation errors to be written to stdout
// so they can more easily be grepd etc.
cmd.Print(verrs.Error())
return errors.Join(asconf.ErrConfigValidation, SilentError)
}
if err != nil {
return err
}

return err
},
}

// flags and configuration settings
commonFlags := getCommonFlags()
res.Flags().AddFlagSet(commonFlags)
res.MarkFlagRequired("aerospike-version")

res.Version = VERSION

return res
}
66 changes: 66 additions & 0 deletions cmd/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//go:build unit
// +build unit

package cmd

import (
"testing"
)

type runTestValidate struct {
flags []string
arguments []string
expectError bool
}

var testValidateArgs = []runTestValidate{
{
flags: []string{},
arguments: []string{"too", "many", "args"},
expectError: true,
},
{
// missing arg to -a-aerospike-version
flags: []string{"--aerospike-version"},
arguments: []string{"../testdata/sources/all_flash_cluster_cr.yaml"},
expectError: true,
},
{
flags: []string{"--aerospike-version"},
arguments: []string{"./bad_extension.ymml"},
expectError: true,
},
{
flags: []string{"--aerospike-version", "bad_version"},
arguments: []string{"../testdata/sources/all_flash_cluster_cr.yaml"},
expectError: true,
},
{
flags: []string{"--aerospike-version", "6.4.0"},
arguments: []string{"./fake_file.yaml"},
expectError: true,
},
{
flags: []string{"--aerospike-version", "6.4.0"},
arguments: []string{"../testdata/cases/server64/server64.yaml"},
expectError: false,
},
{
flags: []string{"--log-level", "debug", "--aerospike-version", "7.0.0"},
arguments: []string{"../testdata/cases/server70/server70.conf"},
expectError: false,
},
}

func TestRunEValidate(t *testing.T) {
cmd := validateCmd

for i, test := range testValidateArgs {
cmd.Parent().ParseFlags(test.flags)
cmd.ParseFlags(test.flags)
err := cmd.RunE(cmd, test.arguments)
if test.expectError == (err == nil) {
t.Fatalf("case: %d, expectError: %v does not match err: %v", i, test.expectError, err)
}
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/opencontainers/image-spec v1.0.2
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
gopkg.in/yaml.v3 v3.0.1
)

Expand All @@ -25,7 +26,6 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/aerospike/aerospike-management-lib v0.0.0-20231025222637-439d643badb8 h1:DhmnlRVu4CURbZhA6oTSyTUSKaG3XWYOviEnyxOzy0g=
github.com/aerospike/aerospike-management-lib v0.0.0-20231025222637-439d643badb8/go.mod h1:O4v2oGl4VjG9KwYJoSVEwZXv1PUB4ioKAsrm2tczJPQ=
github.com/aerospike/aerospike-management-lib v0.0.0-20231025224657-765f71b4994d h1:WqKtqOqdZ41/WvbPV/3tGa4KNXZOOTdhwz/K1+P2CuI=
github.com/aerospike/aerospike-management-lib v0.0.0-20231025224657-765f71b4994d/go.mod h1:O4v2oGl4VjG9KwYJoSVEwZXv1PUB4ioKAsrm2tczJPQ=
github.com/aerospike/aerospike-management-lib v0.0.0-20231106202816-b2438dbb7e03 h1:Od7oCSBCTfpRmk73fbNGXA53U1in38iNSklCEwMMZFc=
github.com/aerospike/aerospike-management-lib v0.0.0-20231106202816-b2438dbb7e03/go.mod h1:O4v2oGl4VjG9KwYJoSVEwZXv1PUB4ioKAsrm2tczJPQ=
github.com/bombsimon/logrusr/v4 v4.0.0 h1:Pm0InGphX0wMhPqC02t31onlq9OVyJ98eP/Vh63t1Oo=
Expand Down
Loading

0 comments on commit f7e7e88

Please sign in to comment.