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

Tools 2715 add the validate command #23

Merged
merged 10 commits into from
Nov 14, 2023
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
dwelch-spike marked this conversation as resolved.
Show resolved Hide resolved
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()
dwelch-spike marked this conversation as resolved.
Show resolved Hide resolved
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