From 7cfc5468b6536eedf1f9d0f5e4bdd62a1564a6ac Mon Sep 17 00:00:00 2001 From: Jesse Schmidt Date: Mon, 8 Jan 2024 17:57:16 -0600 Subject: [PATCH] wip --- .gitmodules | 3 - asconf/asconf.go | 256 ----------------- asconf/asconf_test.go | 279 ------------------ cmd/convert.go | 80 ++---- cmd/convert_test.go | 1 - cmd/diff.go | 35 +-- cmd/diff_test.go | 1 - cmd/generate.go | 129 +++++---- cmd/root.go | 24 +- cmd/root_test.go | 8 +- cmd/utils.go | 72 +++-- cmd/utils_test.go | 14 +- cmd/validate.go | 17 +- cmd/validate_test.go | 1 - conf/config_handler.go | 13 + conf/config_handler_mock.go | 35 +++ conf/config_marshaller.go | 34 +++ conf/config_marshaller_test.go | 71 +++++ conf/config_validator.go | 110 +++++++ conf/config_validator_test.go | 83 ++++++ conf/constants.go | 13 + asconf/utils.go => conf/loader.go | 316 ++++++++++++--------- {asconf => conf}/metadata/metadata.go | 0 {asconf => conf}/metadata/metadata_test.go | 5 +- constants.go | 9 + go.mod | 9 +- go.sum | 18 +- integration_test.go | 167 ++++++----- 28 files changed, 841 insertions(+), 962 deletions(-) delete mode 100644 .gitmodules delete mode 100644 asconf/asconf.go delete mode 100644 asconf/asconf_test.go create mode 100644 conf/config_handler.go create mode 100644 conf/config_handler_mock.go create mode 100644 conf/config_marshaller.go create mode 100644 conf/config_marshaller_test.go create mode 100644 conf/config_validator.go create mode 100644 conf/config_validator_test.go create mode 100644 conf/constants.go rename asconf/utils.go => conf/loader.go (67%) rename {asconf => conf}/metadata/metadata.go (100%) rename {asconf => conf}/metadata/metadata_test.go (97%) create mode 100644 constants.go diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index fe2863f..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "schemas"] - path = schema/schemas - url = https://github.com/aerospike/schemas diff --git a/asconf/asconf.go b/asconf/asconf.go deleted file mode 100644 index c88a552..0000000 --- a/asconf/asconf.go +++ /dev/null @@ -1,256 +0,0 @@ -package asconf - -import ( - "bytes" - "errors" - "fmt" - "sort" - "strings" - - "github.com/aerospike/aerospike-management-lib/asconfig" - "github.com/go-logr/logr" - "github.com/sirupsen/logrus" - "gopkg.in/yaml.v3" -) - -type Format string - -const ( - Invalid Format = "" - YAML Format = "yaml" - AsConfig Format = "asconfig" -) - -var ( - ErrInvalidFormat = fmt.Errorf("invalid config format") - ErrConfigValidation = fmt.Errorf("error while validating config") -) - -// TODO maybe use mockery here -type confMarshalValidator interface { - IsValid(log logr.Logger, version string) (bool, []*asconfig.ValidationErr, error) - ToMap() *asconfig.Conf - ToConfFile() asconfig.DotConf - GetFlatMap() *asconfig.Conf -} - -type asconf struct { - cfg confMarshalValidator - logger *logrus.Logger - managementLibLogger logr.Logger - srcFmt Format - // TODO decouple output format from asconf, probably pass it as an arg to marshal text - outFmt Format - src []byte - aerospikeVersion string -} - -func NewAsconf(source []byte, srcFmt, outFmt Format, aerospikeVersion string, logger *logrus.Logger, managementLibLogger logr.Logger) (*asconf, error) { - - ac := &asconf{ - logger: logger, - managementLibLogger: managementLibLogger, - srcFmt: srcFmt, - outFmt: outFmt, - src: source, - aerospikeVersion: aerospikeVersion, - } - - // sets ac.cfg - err := ac.load() - - return ac, err -} - -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, tempVerrs, err := ac.cfg.IsValid(ac.managementLibLogger, ac.aerospikeVersion) - - verrs := ValidationErrors{} - for _, v := range tempVerrs { - verr := ValidationErr{ - ValidationErr: *v, - } - verrs.Errors = append(verrs.Errors, verr) - } - - if !valid || err != nil || len(verrs.Errors) > 0 { - return &verrs, errors.Join(ErrConfigValidation, err) - } - - return nil, nil -} - -func (ac *asconf) MarshalText() (text []byte, err error) { - - switch ac.outFmt { - case AsConfig: - text = []byte(ac.cfg.ToConfFile()) - case YAML: - m := ac.cfg.ToMap() - text, err = yaml.Marshal(m) - default: - err = fmt.Errorf("%w %s", ErrInvalidFormat, ac.outFmt) - } - - return -} - -func (ac *asconf) GetIntermediateConfig() map[string]any { - return *ac.cfg.GetFlatMap() -} - -func (ac *asconf) load() (err error) { - - switch ac.srcFmt { - case YAML: - err = ac.loadYAML() - case AsConfig: - err = ac.loadAsConf() - default: - return fmt.Errorf("%w %s", ErrInvalidFormat, ac.srcFmt) - } - - if err != nil { - return err - } - - // recreate the management lib config - // with a sorted config map so that output - // is always in the same order - cmap := *ac.cfg.ToMap() - - mutateMap(cmap, []mapping{ - sortLists, - }) - - ac.cfg, err = asconfig.NewMapAsConfig( - ac.managementLibLogger, - ac.aerospikeVersion, - cmap, - ) - - return -} - -func (ac *asconf) loadYAML() error { - - var data map[string]any - - err := yaml.Unmarshal(ac.src, &data) - if err != nil { - return err - } - - c, err := asconfig.NewMapAsConfig( - ac.managementLibLogger, - ac.aerospikeVersion, - data, - ) - - if err != nil { - return fmt.Errorf("failed to initialize asconfig from yaml: %w", err) - } - - ac.cfg = c - - return nil -} - -func (ac *asconf) loadAsConf() error { - - reader := bytes.NewReader(ac.src) - - c, err := asconfig.FromConfFile(ac.managementLibLogger, ac.aerospikeVersion, reader) - if err != nil { - return fmt.Errorf("failed to parse asconfig file: %w", err) - } - - // the aerospike management lib parses asconfig files into - // a format that its validation rejects - // this is because the schema files are meant to - // validate the aerospike kubernetes operator's asconfig yaml format - // so we modify the map here to match that format - cmap := *c.ToMap() - - mutateMap(cmap, []mapping{ - typedContextsToObject, - toPlural, - }) - - c, err = asconfig.NewMapAsConfig( - ac.managementLibLogger, - ac.aerospikeVersion, - cmap, - ) - - if err != nil { - return err - } - - ac.cfg = c - - return nil -} diff --git a/asconf/asconf_test.go b/asconf/asconf_test.go deleted file mode 100644 index f5551df..0000000 --- a/asconf/asconf_test.go +++ /dev/null @@ -1,279 +0,0 @@ -//go:build unit -// +build unit - -package asconf - -import ( - "fmt" - "reflect" - "testing" - - "github.com/aerospike/aerospike-management-lib/asconfig" - "github.com/go-logr/logr" - "github.com/sirupsen/logrus" -) - -type mockCFG struct { - valid bool - err error - validationErrors []*asconfig.ValidationErr - confMap *asconfig.Conf - confText string - flatConf *asconfig.Conf -} - -func (o *mockCFG) IsValid(log logr.Logger, version string) (bool, []*asconfig.ValidationErr, error) { - return o.valid, o.validationErrors, o.err -} - -func (o *mockCFG) ToMap() *asconfig.Conf { - return o.confMap -} - -func (o *mockCFG) ToConfFile() asconfig.DotConf { - return o.confText -} - -func (o *mockCFG) GetFlatMap() *asconfig.Conf { - return o.flatConf -} - -var _testYaml = ` -namespaces: - - index-type: - mounts: - - /test/dev/xvdf-index - mounts-size-limit: 4294967296 - type: flash - memory-size: 3000000000 - name: test - replication-factor: 2 - storage-engine: - devices: - - /test/dev/xvdf - type: device -` - -// { -// name: "yaml-to-conf", -// fields: fields{ -// cfg: &mockCFG{ -// valid: true, -// err: nil, -// validationErrors: []*asconfig.ValidationErr{}, -// confMap: &asconfig.Conf{ -// "namespaces": []string{"ns1", "ns1"}, -// }, -// confText: "namespace ns1 {}\n namespace ns2 {}", -// flatConf: &asconfig.Conf{ -// "namespaces.ns1": "device", -// "namespaces.ns2": "memory", -// }, -// }, -// logger: logrus.New(), -// managementLibLogger: logrusr.New(logrus.New()), -// srcFmt: YAML, -// outFmt: AsConfig, -// src: []byte(_testYaml), -// aerospikeVersion: "6.2.0.2", -// }, -// wantErr: false, -// }, - -func Test_asconf_Validate(t *testing.T) { - type fields struct { - cfg confMarshalValidator - logger *logrus.Logger - managementLibLogger logr.Logger - srcFmt Format - outFmt Format - src []byte - aerospikeVersion string - } - tests := []struct { - name string - fields fields - wantErr bool - }{ - { - name: "pos1", - fields: fields{ - cfg: &mockCFG{ - valid: true, - err: nil, - validationErrors: []*asconfig.ValidationErr{}, - }, - logger: logrus.New(), - }, - wantErr: false, - }, - { - name: "neg1", - fields: fields{ - cfg: &mockCFG{ - valid: false, - err: nil, - validationErrors: []*asconfig.ValidationErr{}, - }, - logger: logrus.New(), - }, - wantErr: true, - }, - { - name: "neg2", - fields: fields{ - cfg: &mockCFG{ - valid: true, - err: fmt.Errorf("test_err"), - validationErrors: []*asconfig.ValidationErr{}, - }, - logger: logrus.New(), - }, - wantErr: true, - }, - { - name: "neg3", - fields: fields{ - cfg: &mockCFG{ - valid: true, - err: nil, - validationErrors: []*asconfig.ValidationErr{{}}, - }, - logger: logrus.New(), - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ac := &asconf{ - cfg: tt.fields.cfg, - logger: tt.fields.logger, - managementLibLogger: tt.fields.managementLibLogger, - srcFmt: tt.fields.srcFmt, - outFmt: tt.fields.outFmt, - src: tt.fields.src, - aerospikeVersion: tt.fields.aerospikeVersion, - } - if _, err := ac.Validate(); (err != nil) != tt.wantErr { - t.Errorf("asconf.Validate() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func Test_asconf_MarshalText(t *testing.T) { - type fields struct { - cfg confMarshalValidator - logger *logrus.Logger - managementLibLogger logr.Logger - srcFmt Format - outFmt Format - src []byte - aerospikeVersion string - } - tests := []struct { - name string - fields fields - wantText []byte - wantErr bool - }{ - { - name: "valid asconfig format", - fields: fields{ - cfg: &mockCFG{ - confText: "namespace ns1 {}\n namespace ns2 {}", - }, - outFmt: AsConfig, - }, - wantErr: false, - wantText: []byte("namespace ns1 {}\n namespace ns2 {}"), - }, - { - name: "valid yaml format", - fields: fields{ - cfg: &mockCFG{ - confMap: &asconfig.Conf{ - "namespaces": "ns1", - }, - confText: "", - }, - outFmt: YAML, - }, - wantErr: false, - wantText: []byte("namespaces: ns1\n"), - }, - { - name: "invalid format", - fields: fields{ - outFmt: Invalid, - }, - wantErr: true, - wantText: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ac := &asconf{ - cfg: tt.fields.cfg, - logger: tt.fields.logger, - managementLibLogger: tt.fields.managementLibLogger, - srcFmt: tt.fields.srcFmt, - outFmt: tt.fields.outFmt, - src: tt.fields.src, - aerospikeVersion: tt.fields.aerospikeVersion, - } - gotText, err := ac.MarshalText() - if (err != nil) != tt.wantErr { - t.Errorf("asconf.MarshalText() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(gotText, tt.wantText) { - t.Errorf("asconf.MarshalText() = %v, want %v", gotText, tt.wantText) - } - }) - } -} - -func Test_asconf_GetIntermediateConfig(t *testing.T) { - type fields struct { - cfg confMarshalValidator - logger *logrus.Logger - managementLibLogger logr.Logger - srcFmt Format - outFmt Format - src []byte - aerospikeVersion string - } - tests := []struct { - name string - fields fields - want map[string]any - }{ - { - name: "return flat conf", - fields: fields{ - cfg: &mockCFG{ - flatConf: &configMap{"ns": 1}, - }, - }, - want: map[string]any{"ns": 1}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ac := &asconf{ - cfg: tt.fields.cfg, - logger: tt.fields.logger, - managementLibLogger: tt.fields.managementLibLogger, - srcFmt: tt.fields.srcFmt, - outFmt: tt.fields.outFmt, - src: tt.fields.src, - aerospikeVersion: tt.fields.aerospikeVersion, - } - if got := ac.GetIntermediateConfig(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("asconf.GetIntermediateConfig() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/cmd/convert.go b/cmd/convert.go index 266aa22..30a6983 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -4,12 +4,10 @@ import ( "errors" "fmt" "os" - "path/filepath" - "strings" "github.com/aerospike/aerospike-management-lib/asconfig" - "github.com/aerospike/asconfig/asconf" - "github.com/aerospike/asconfig/asconf/metadata" + "github.com/aerospike/asconfig/conf" + "github.com/aerospike/asconfig/conf/metadata" "github.com/spf13/cobra" ) @@ -70,12 +68,12 @@ func newConvertCmd() *cobra.Command { srcPath = args[0] } - version, err := cmd.Flags().GetString("aerospike-version") + asVersion, err := cmd.Flags().GetString("aerospike-version") if err != nil { return err } - logger.Debugf("Processing flag aerospike-version value=%s", version) + logger.Debugf("Processing flag aerospike-version value=%s", asVersion) force, err := cmd.Flags().GetBool("force") if err != nil { @@ -89,63 +87,60 @@ func newConvertCmd() *cobra.Command { return err } - logger.Debugf("Processing flag format value=%v", srcFormat) - fdata, err := os.ReadFile(srcPath) if err != nil { return err } - var outFmt asconf.Format + var outFmt conf.Format switch srcFormat { - case asconf.AsConfig: - outFmt = asconf.YAML - case asconf.YAML: - outFmt = asconf.AsConfig + case conf.AsConfig: + outFmt = conf.YAML + case conf.YAML: + outFmt = conf.AsConfig default: return fmt.Errorf("%w: %s", errInvalidFormat, srcFormat) } // if the version option is empty, // try populating from the metadata - if version == "" { - version, err = getMetaDataItem(fdata, metaKeyAerospikeVersion) + if asVersion == "" { + asVersion, err = getMetaDataItem(fdata, metaKeyAerospikeVersion) if err != nil && !force { return errors.Join(errMissingAerospikeVersion, err) } } - conf, err := asconf.NewAsconf( - fdata, - srcFormat, - outFmt, - version, - logger, - managementLibLogger, - ) + // load + asconfig, err := conf.NewASConfigFromBytes(mgmtLibLogger, fdata, srcFormat) if err != nil { return err } + // validate if !force { - verrs, err := conf.Validate() + verrs, err := conf.NewConfigValidator(asconfig, mgmtLibLogger, asVersion).Validate() if err != nil || verrs != nil { return errors.Join(err, verrs) } } - out, err := conf.MarshalText() + // convert + out, err := conf.NewConfigMarshaller(asconfig, outFmt).MarshalText() if err != nil { return err } // prepend metadata to the config output - mtext, err := genMetaDataText(metaDataArgs{ - src: fdata, - aerospikeVersion: version, - asconfigVersion: VERSION, - }) + mtext, err := genMetaDataText( + fdata, + nil, + map[string]string{ + metaKeyAerospikeVersion: asVersion, + metaKeyAsconfigVersion: VERSION, + }, + ) if err != nil { return err } @@ -156,23 +151,9 @@ func newConvertCmd() *cobra.Command { return err } - if stat, err := os.Stat(outputPath); !errors.Is(err, os.ErrNotExist) && stat.IsDir() { - // output path is a directory so write a new file to it - outFileName := filepath.Base(srcPath) - if srcPath == os.Stdin.Name() { - outFileName = defaultOutputFileName - } - - outFileName = strings.TrimSuffix(outFileName, filepath.Ext(outFileName)) - - outputPath = filepath.Join(outputPath, outFileName) - if outFmt == asconf.YAML { - outputPath += ".yaml" - } else if outFmt == asconf.AsConfig { - outputPath += ".conf" - } else { - return fmt.Errorf("output format unrecognized %w", errInvalidFormat) - } + err = CheckIsDir(&outputPath, outFmt) + if err != nil { + return err } var outFile *os.File @@ -261,10 +242,11 @@ 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 - commonFlags := getCommonFlags() - res.Flags().AddFlagSet(commonFlags) + asCommonFlags := getCommonFlags() + res.Flags().AddFlagSet(asCommonFlags) 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") + res.Flags().StringP("format", "F", "conf", "The format of the source file(s). Valid options are: yaml, yml, and conf.") res.Version = VERSION diff --git a/cmd/convert_test.go b/cmd/convert_test.go index 94e7eaf..20145b4 100644 --- a/cmd/convert_test.go +++ b/cmd/convert_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package cmd diff --git a/cmd/diff.go b/cmd/diff.go index 7536aea..2e2c995 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -8,7 +8,7 @@ import ( "sort" "strings" - "github.com/aerospike/asconfig/asconf" + "github.com/aerospike/asconfig/conf" "github.com/spf13/cobra" ) @@ -19,7 +19,7 @@ const ( ) var ( - errDiffConfigsDiffer = errors.Join(fmt.Errorf("configuration files are not equal"), SilentError) + errDiffConfigsDiffer = errors.Join(fmt.Errorf("configuration files are not equal"), ErrSilent) errDiffTooFewArgs = fmt.Errorf("diff requires atleast %d file paths as arguments", diffArgMin) errDiffTooManyArgs = fmt.Errorf("diff requires no more than %d file paths as arguments", diffArgMax) ) @@ -85,38 +85,25 @@ func newDiffCmd() *cobra.Command { // not performing any validation so server version is "" (not needed) // won't be marshaling these configs to text so use Invalid output format - // TODO decouple output format from asconf, probably pass it as an arg to marshal text - conf1, err := asconf.NewAsconf( - f1, - fmt1, - asconf.Invalid, - "", - logger, - managementLibLogger, - ) + // TODO decouple output format from asconf, probably pass it as an + // arg to marshal text + conf1, err := conf.NewASConfigFromBytes(mgmtLibLogger, f1, fmt1) if err != nil { return err } - conf2, err := asconf.NewAsconf( - f2, - fmt2, - asconf.Invalid, - "", - logger, - managementLibLogger, - ) + conf2, err := conf.NewASConfigFromBytes(mgmtLibLogger, f2, fmt2) if err != nil { return err } // get flattened config maps - map1 := conf1.GetIntermediateConfig() - map2 := conf2.GetIntermediateConfig() + map1 := conf1.GetFlatMap() + map2 := conf2.GetFlatMap() diffs := diffFlatMaps( - map1, - map2, + *map1, + *map2, ) if len(diffs) > 0 { @@ -129,6 +116,8 @@ func newDiffCmd() *cobra.Command { }, } + res.Flags().StringP("format", "F", "conf", "The format of the source file(s). Valid options are: yaml, yml, and conf.") + return res } diff --git a/cmd/diff_test.go b/cmd/diff_test.go index 596c516..96f04f0 100644 --- a/cmd/diff_test.go +++ b/cmd/diff_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package cmd diff --git a/cmd/generate.go b/cmd/generate.go index ed67a3a..5422e8c 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -4,21 +4,15 @@ import ( "errors" "fmt" "os" - "path/filepath" - "runtime/pprof" - "strings" - as "github.com/aerospike/aerospike-client-go/v6" "github.com/aerospike/aerospike-management-lib/asconfig" "github.com/aerospike/aerospike-management-lib/info" - "github.com/aerospike/asconfig/asconf" + "github.com/aerospike/asconfig/conf" + "github.com/aerospike/tools-common-go/client" + "github.com/aerospike/tools-common-go/flags" "github.com/spf13/cobra" ) -const ( - generateArgMax = 1 -) - func init() { rootCmd.AddCommand(generateCmd) } @@ -26,77 +20,90 @@ func init() { var generateCmd = newGenerateCmd() func newGenerateCmd() *cobra.Command { + asCommonFlags := flags.NewDefaultAerospikeFlags() + disclaimer := []byte(`# +# This configuration file is generated by asconfig, this feature is currently in beta. +# We appreciate your feedback on any issues encountered. These can be reported +# to our support team or via GitHub. Please ensure to verify the configuration +# file before use. Current limitations include the inability to generate the +# following contexts and parameters: logging.syslog, mod-lua, service.user, +# service.group. Please note that this configuration file may not be compatible +# with all versions of Aerospike or the Community Edition.`) res := &cobra.Command{ Use: "generate [flags]", - Short: "Generate an configuration file from a running Aerospike node.", - Long: `TODO`, + Short: "BETA: Generate a configuration file from a running Aerospike node.", + Long: `BETA: Generate a configuration file from a running Aerospike node. This can be useful if you have changed the configuration of a node dynamically (e.g. xdr) and would like to persist the changes.`, RunE: func(cmd *cobra.Command, args []string) error { logger.Debug("Running generate command") - // write stdout by default - var dstPath string - if len(args) == 0 { - dstPath = os.Stdout.Name() - } else { - dstPath = args[0] + outputPath, err := cmd.Flags().GetString("output") + if err != nil { + return err } - outFormat := asconf.AsConfig - - logger.Debugf("Processing flag format value=%v", outFormat) + outFormat, err := getConfFileFormat(outputPath, cmd) + if err != nil { + return err + } logger.Debugf("Generating config from Aerospike node") - asPolicy := as.NewClientPolicy() - asPolicy.User = "admin" - asPolicy.Password = "admin" - asinfo := info.NewAsInfo(managementLibLogger, as.NewHost("172.17.0.5", 3000), asPolicy) - f, err := os.Create("profile.prof") + asCommonConfig := client.NewDefaultAerospikeHostConfig() + + flags.SetAerospikeConf(asCommonConfig, asCommonFlags) + + asPolicy, err := asCommonConfig.NewClientPolicy() + if err != nil { - logger.Fatal(err) + return errors.Join(fmt.Errorf("unable to create client policy"), err) } - pprof.StartCPUProfile(f) - defer pprof.StopCPUProfile() + logger.Infof("Retrieving Aerospike configuration from node %s", &asCommonFlags.Seeds) - generatedConf, err := asconfig.GenerateConf(managementLibLogger, asinfo, true) + asHosts := asCommonConfig.NewHosts() + asinfo := info.NewAsInfo(mgmtLibLogger, asHosts[0], asPolicy) + generatedConf, err := asconfig.GenerateConf(mgmtLibLogger, asinfo, true) if err != nil { return errors.Join(fmt.Errorf("unable to generate config file"), err) } - conf, err := asconfig.NewMapAsConfig(managementLibLogger, generatedConf.Version, generatedConf.Conf) + asconfig, err := asconfig.NewMapAsConfig(mgmtLibLogger, generatedConf.Version, generatedConf.Conf) if err != nil { return errors.Join(fmt.Errorf("unable to parse the generated conf file"), err) } - confFile := conf.ToConfFile() + marshaller := conf.NewConfigMarshaller(asconfig, outFormat) - if stat, err := os.Stat(dstPath); !errors.Is(err, os.ErrNotExist) && stat.IsDir() { - // output path is a directory so write a new file to it - outFileName := filepath.Base(dstPath) - if dstPath == os.Stdin.Name() { - outFileName = defaultOutputFileName - } + fdata, err := marshaller.MarshalText() + if err != nil { + return errors.Join(fmt.Errorf("unable to marshal the generated conf file"), err) + } - outFileName = strings.TrimSuffix(outFileName, filepath.Ext(outFileName)) + err = CheckIsDir(&outputPath, outFormat) - dstPath = filepath.Join(dstPath, outFileName) - if outFormat == asconf.YAML { - dstPath += ".yaml" - } else if outFormat == asconf.AsConfig { - dstPath += ".conf" - } else { - return fmt.Errorf("output format unrecognized %w", errInvalidFormat) - } + if err != nil { + return err + } + + mdata := map[string]string{ + metaKeyAerospikeVersion: generatedConf.Version, + metaKeyAsconfigVersion: VERSION, + } + // prepend metadata to the config output + mtext, err := genMetaDataText(fdata, disclaimer, mdata) + if err != nil { + return err } + fdata = append(mtext, fdata...) + var outFile *os.File - if dstPath == os.Stdout.Name() { + if outputPath == os.Stdout.Name() { outFile = os.Stdout } else { - outFile, err = os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + outFile, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } @@ -104,24 +111,26 @@ func newGenerateCmd() *cobra.Command { defer outFile.Close() } - logger.Debugf("Writing converted data to: %s", dstPath) - _, err = outFile.Write([]byte(confFile)) - return err + logger.Debugf("Writing converted data to: %s", outputPath) + _, err = outFile.Write(fdata) - }, - PreRunE: func(cmd *cobra.Command, args []string) error { + logger.Warning( + "Community Edition is not supported. Generated static configuration does not save logging.syslog, mod-lua, service.user and service.group", + ) + logger.Warning( + "This feature is currently in beta. Use at your own risk and please report any issue to support.", + ) - return nil + return err }, } - // flags and configuration settings - // aerospike-version is marked required in this cmd's PreRun if the --force flag is not provided - // commonFlags := getCommonFlags() - // res.Flags().AddFlagSet(commonFlags) - // res.Flags().BoolP("force", "f", false, "Override checks for supported server version and config validation") - res.Version = VERSION + res.Flags().AddFlagSet( + flags.SetAerospikeFlags(asCommonFlags, flags.DefaultWrapHelpString), + ) + res.Flags().StringP("output", "o", os.Stdout.Name(), flags.DefaultWrapHelpString("File path to write output to")) + res.Flags().StringP("format", "F", "conf", "The format of the destination file(s). Valid options are: yaml, yml, and conf.") return res } diff --git a/cmd/root.go b/cmd/root.go index 777022d..3f70db4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,8 +10,6 @@ import ( "github.com/aerospike/asconfig/schema" - "github.com/aerospike/asconfig/asconf" - "github.com/aerospike/aerospike-management-lib/asconfig" "github.com/bombsimon/logrusr/v4" "github.com/go-logr/logr" @@ -53,16 +51,6 @@ func newRootCmd() *cobra.Command { logger.SetLevel(lvlCode) - formatString, err := cmd.Flags().GetString("format") - if err != nil { - multiErr = fmt.Errorf("%w, %w", multiErr, err) - } - - _, err = asconf.ParseFmtString(formatString) - if err != nil && formatString != "" { - multiErr = fmt.Errorf("%w, %w", multiErr, err) - } - return multiErr }, } @@ -73,7 +61,7 @@ func newRootCmd() *cobra.Command { logLevelUsage := fmt.Sprintf("Set the logging detail level. Valid levels are: %v", log.GetLogLevels()) res.PersistentFlags().StringP("log-level", "l", "info", logLevelUsage) res.PersistentFlags().BoolP("version", "V", false, "Version for asconfig.") - res.PersistentFlags().StringP("format", "F", "conf", "The format of the source file(s). Valid options are: yaml, yml, and conf.") + res.PersistentFlags().BoolP("help", "u", false, "Display help information") res.SilenceErrors = true res.SilenceUsage = true @@ -81,7 +69,7 @@ func newRootCmd() *cobra.Command { res.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { logger.Error(err) cmd.Println(cmd.UsageString()) - return errors.Join(err, SilentError) + return errors.Join(err, ErrSilent) }) return res @@ -92,7 +80,7 @@ func newRootCmd() *cobra.Command { func Execute() { err := rootCmd.Execute() if err != nil { - if !errors.Is(err, SilentError) { + if !errors.Is(err, ErrSilent) { // handle wrapped errors errs := strings.Split(err.Error(), "\n") @@ -105,7 +93,7 @@ func Execute() { } var logger *logrus.Logger -var managementLibLogger logr.Logger +var mgmtLibLogger logr.Logger func init() { logger = logrus.New() @@ -120,6 +108,6 @@ func init() { panic(err) } - managementLibLogger = logrusr.New(logger) - asconfig.InitFromMap(managementLibLogger, schemaMap) + mgmtLibLogger = logrusr.New(logger) + asconfig.InitFromMap(mgmtLibLogger, schemaMap) } diff --git a/cmd/root_test.go b/cmd/root_test.go index 1257b0c..4fd2ef8 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1,12 +1,12 @@ //go:build unit -// +build unit package cmd import ( "errors" - "github.com/aerospike/asconfig/asconf" "testing" + + "github.com/aerospike/asconfig/conf" ) type preTestRoot struct { @@ -37,14 +37,14 @@ var preTestsRoot = []preTestRoot{ flags: []string{"--format", "bad_fmt"}, arguments: []string{}, expectedErrors: []error{ - asconf.ErrInvalidFormat, + conf.ErrInvalidFormat, }, }, { flags: []string{"-F", "bad_fmt"}, arguments: []string{}, expectedErrors: []error{ - asconf.ErrInvalidFormat, + conf.ErrInvalidFormat, }, }, } diff --git a/cmd/utils.go b/cmd/utils.go index 22286e7..4fd8ca6 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -3,11 +3,12 @@ package cmd import ( "errors" "fmt" + "os" "path/filepath" "strings" - "github.com/aerospike/asconfig/asconf" - "github.com/aerospike/asconfig/asconf/metadata" + "github.com/aerospike/asconfig/conf" + "github.com/aerospike/asconfig/conf/metadata" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -25,31 +26,28 @@ type metaDataArgs struct { asconfigVersion string } -func genMetaDataText(args metaDataArgs) ([]byte, error) { +func genMetaDataText(src []byte, msg []byte, mdata map[string]string) ([]byte, error) { metaHeader := "# *** Aerospike Metadata Generated by Asconfig ***" - src := args.src - aev := args.aerospikeVersion - asv := args.asconfigVersion - - mdata := map[string]string{} err := metadata.Unmarshal(src, mdata) if err != nil { return nil, err } - mdata[metaKeyAerospikeVersion] = aev - mdata[metaKeyAsconfigVersion] = asv - mtext, err := metadata.Marshal(mdata) if err != nil { return nil, err } metaFooter := "# *** End Aerospike Metadata ***" + strMsg := string(msg) + + if len(msg) > 0 { + strMsg = strMsg + "\n#\n" + } - mtext = []byte(fmt.Sprintf("%s\n%s%s\n", metaHeader, mtext, metaFooter)) + mtext = []byte(fmt.Sprintf("%s\n%s%s%s\n\n", metaHeader, strMsg, mtext, metaFooter)) return mtext, nil } @@ -92,27 +90,67 @@ func getCommonFlags() *pflag.FlagSet { // this function implements the defaults scheme for file formats in asconfig // if the --format flag is defined use that, else if the path has an extension // use that, else use the default value from --format -func getConfFileFormat(path string, cmd *cobra.Command) (asconf.Format, error) { +func getConfFileFormat(path string, cmd *cobra.Command) (conf.Format, error) { ext := filepath.Ext(path) ext = strings.TrimPrefix(ext, ".") fmtStr, err := cmd.Flags().GetString("format") if err != nil { - return asconf.Invalid, err + return conf.Invalid, err } + logger.Debugf("Processing flag format value=%v", fmtStr) + // if the user did not supply format, and // the input file has an extension, overwrite it with ext if !cmd.Flags().Changed("format") && ext != "" { fmtStr = ext } - fmt, err := asconf.ParseFmtString(fmtStr) + fmt, err := ParseFmtString(fmtStr) if err != nil { - return asconf.Invalid, err + return conf.Invalid, err } return fmt, nil } -var SilentError = errors.New("SILENT") +var ErrSilent = errors.New("SILENT") + +func ParseFmtString(in string) (f conf.Format, err error) { + + switch strings.ToLower(in) { + case "yaml", "yml": + f = conf.YAML + case "asconfig", "conf", "asconf": + f = conf.AsConfig + default: + f = conf.Invalid + err = fmt.Errorf("%w: %s", conf.ErrInvalidFormat, in) + } + + return +} + +func CheckIsDir(outputPath *string, outFormat conf.Format) error { + if stat, err := os.Stat(*outputPath); !errors.Is(err, os.ErrNotExist) && stat.IsDir() { + // output path is a directory so write a new file to it + outFileName := filepath.Base(*outputPath) + if *outputPath == os.Stdin.Name() { + outFileName = defaultOutputFileName + } + + outFileName = strings.TrimSuffix(outFileName, filepath.Ext(outFileName)) + + *outputPath = filepath.Join(*outputPath, outFileName) + if outFormat == conf.YAML { + *outputPath += ".yaml" + } else if outFormat == conf.AsConfig { + *outputPath += ".conf" + } else { + return fmt.Errorf("output format unrecognized %w", errInvalidFormat) + } + } + + return nil +} diff --git a/cmd/utils_test.go b/cmd/utils_test.go index 571672c..8a29ce5 100644 --- a/cmd/utils_test.go +++ b/cmd/utils_test.go @@ -1,13 +1,13 @@ //go:build unit -// +build unit package cmd import ( - "github.com/aerospike/asconfig/asconf" "reflect" "testing" + "github.com/aerospike/asconfig/conf" + "github.com/spf13/cobra" ) @@ -28,7 +28,7 @@ func Test_getConfFileFormat(t *testing.T) { tests := []struct { name string args args - want asconf.Format + want conf.Format wantErr bool }{ { @@ -37,7 +37,7 @@ func Test_getConfFileFormat(t *testing.T) { path: "conf.yaml", cmd: &mockCmdNoFmt, }, - want: asconf.YAML, + want: conf.YAML, wantErr: false, }, { @@ -46,7 +46,7 @@ func Test_getConfFileFormat(t *testing.T) { path: "conf.conf", cmd: &mockCmdNoFmt, }, - want: asconf.AsConfig, + want: conf.AsConfig, wantErr: false, }, { @@ -55,7 +55,7 @@ func Test_getConfFileFormat(t *testing.T) { path: "conf.yaml", cmd: &mockCmd, }, - want: asconf.AsConfig, + want: conf.AsConfig, wantErr: false, }, { @@ -64,7 +64,7 @@ func Test_getConfFileFormat(t *testing.T) { path: "../testdata/sources/all_flash_cluster_cr.bad", cmd: &mockCmdNoFmt, }, - want: asconf.Invalid, + want: conf.Invalid, wantErr: true, }, } diff --git a/cmd/validate.go b/cmd/validate.go index c284f9b..ca6626a 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -5,7 +5,7 @@ import ( "fmt" "os" - "github.com/aerospike/asconfig/asconf" + "github.com/aerospike/asconfig/conf" "github.com/spf13/cobra" ) @@ -84,27 +84,18 @@ func newValidateCmd() *cobra.Command { logger.Debugf("Processing flag aerospike-version value=%s", version) - 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, - ) + asconfig, err := conf.NewASConfigFromBytes(mgmtLibLogger, fdata, srcFormat) if err != nil { return err } - verrs, err := conf.Validate() + verrs, err := conf.NewConfigValidator(asconfig, mgmtLibLogger, version).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) + return errors.Join(conf.ErrConfigValidation, ErrSilent) } if err != nil { return err diff --git a/cmd/validate_test.go b/cmd/validate_test.go index 6964af6..9325858 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package cmd diff --git a/conf/config_handler.go b/conf/config_handler.go new file mode 100644 index 0000000..6f55f7b --- /dev/null +++ b/conf/config_handler.go @@ -0,0 +1,13 @@ +package conf + +import ( + "github.com/aerospike/aerospike-management-lib/asconfig" + "github.com/go-logr/logr" +) + +type ConfHandler interface { + IsValid(log logr.Logger, version string) (bool, []*asconfig.ValidationErr, error) + ToMap() *asconfig.Conf + ToConfFile() asconfig.DotConf + GetFlatMap() *asconfig.Conf +} diff --git a/conf/config_handler_mock.go b/conf/config_handler_mock.go new file mode 100644 index 0000000..4a0f5ed --- /dev/null +++ b/conf/config_handler_mock.go @@ -0,0 +1,35 @@ +//go:build unit + +package conf + +import ( + "github.com/aerospike/aerospike-management-lib/asconfig" + "github.com/go-logr/logr" +) + +// TODO: Use a gomock instead. It is more robust in its assertions. + +type mockCFG struct { + valid bool + err error + validationErrors []*asconfig.ValidationErr + confMap *asconfig.Conf + confText string + flatConf *asconfig.Conf +} + +func (o *mockCFG) IsValid(log logr.Logger, version string) (bool, []*asconfig.ValidationErr, error) { + return o.valid, o.validationErrors, o.err +} + +func (o *mockCFG) ToMap() *asconfig.Conf { + return o.confMap +} + +func (o *mockCFG) ToConfFile() asconfig.DotConf { + return o.confText +} + +func (o *mockCFG) GetFlatMap() *asconfig.Conf { + return o.flatConf +} diff --git a/conf/config_marshaller.go b/conf/config_marshaller.go new file mode 100644 index 0000000..5cbcb82 --- /dev/null +++ b/conf/config_marshaller.go @@ -0,0 +1,34 @@ +package conf + +import ( + "fmt" + + "gopkg.in/yaml.v3" +) + +type ConfigMarshaller struct { + Format Format + ConfHandler +} + +func NewConfigMarshaller(conf ConfHandler, format Format) ConfigMarshaller { + return ConfigMarshaller{ + Format: format, + ConfHandler: conf, + } +} + +func (cm ConfigMarshaller) MarshalText() (text []byte, err error) { + + switch cm.Format { + case AsConfig: + text = []byte(cm.ToConfFile()) + case YAML: + m := cm.ToMap() + text, err = yaml.Marshal(m) + default: + err = fmt.Errorf("%w %s", ErrInvalidFormat, cm.Format) + } + + return +} diff --git a/conf/config_marshaller_test.go b/conf/config_marshaller_test.go new file mode 100644 index 0000000..8f4d0eb --- /dev/null +++ b/conf/config_marshaller_test.go @@ -0,0 +1,71 @@ +//go:build unit + +package conf + +import ( + "reflect" + "testing" + + "github.com/aerospike/aerospike-management-lib/asconfig" +) + +func Test_asconf_MarshalText(t *testing.T) { + type fields struct { + cfg ConfHandler + outFmt Format + } + tests := []struct { + name string + fields fields + wantText []byte + wantErr bool + }{ + { + name: "valid asconfig format", + fields: fields{ + cfg: &mockCFG{ + confText: "namespace ns1 {}\n namespace ns2 {}", + }, + outFmt: AsConfig, + }, + wantErr: false, + wantText: []byte("namespace ns1 {}\n namespace ns2 {}"), + }, + { + name: "valid yaml format", + fields: fields{ + cfg: &mockCFG{ + confMap: &asconfig.Conf{ + "namespaces": "ns1", + }, + confText: "", + }, + outFmt: YAML, + }, + wantErr: false, + wantText: []byte("namespaces: ns1\n"), + }, + { + name: "invalid format", + fields: fields{ + outFmt: Invalid, + }, + wantErr: true, + wantText: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ac := NewConfigMarshaller(tt.fields.cfg, tt.fields.outFmt) + gotText, err := ac.MarshalText() + + if (err != nil) != tt.wantErr { + t.Errorf("asconf.MarshalText() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotText, tt.wantText) { + t.Errorf("asconf.MarshalText() = %v, want %v", gotText, tt.wantText) + } + }) + } +} diff --git a/conf/config_validator.go b/conf/config_validator.go new file mode 100644 index 0000000..294686c --- /dev/null +++ b/conf/config_validator.go @@ -0,0 +1,110 @@ +package conf + +import ( + "errors" + "fmt" + "sort" + "strings" + + "github.com/aerospike/aerospike-management-lib/asconfig" + "github.com/go-logr/logr" +) + +var ( + ErrConfigValidation = fmt.Errorf("error while validating config") +) + +type ConfigValidator struct { + ConfHandler + mgmtLogger logr.Logger + version string +} + +func NewConfigValidator(confHandler ConfHandler, mgmtLogger logr.Logger, version string) *ConfigValidator { + return &ConfigValidator{ + ConfHandler: confHandler, + mgmtLogger: mgmtLogger, + version: version, + } +} + +// 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 (cv *ConfigValidator) Validate() (*ValidationErrors, error) { + + valid, tempVerrs, err := cv.IsValid(cv.mgmtLogger, cv.version) + + verrs := ValidationErrors{} + for _, v := range tempVerrs { + verr := ValidationErr{ + ValidationErr: *v, + } + verrs.Errors = append(verrs.Errors, verr) + } + + if !valid || err != nil || len(verrs.Errors) > 0 { + return &verrs, errors.Join(ErrConfigValidation, err) + } + + return nil, nil +} + +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 +} diff --git a/conf/config_validator_test.go b/conf/config_validator_test.go new file mode 100644 index 0000000..dc802a7 --- /dev/null +++ b/conf/config_validator_test.go @@ -0,0 +1,83 @@ +//go:build unit + +package conf + +import ( + "fmt" + "testing" + + "github.com/aerospike/aerospike-management-lib/asconfig" + "github.com/go-logr/logr" + "github.com/sirupsen/logrus" +) + +func Test_asconf_Validate(t *testing.T) { + type fields struct { + cfg ConfHandler + logger *logrus.Logger + managementLibLogger logr.Logger + aerospikeVersion string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "pos1", + fields: fields{ + cfg: &mockCFG{ + valid: true, + err: nil, + validationErrors: []*asconfig.ValidationErr{}, + }, + logger: logrus.New(), + }, + wantErr: false, + }, + { + name: "neg1", + fields: fields{ + cfg: &mockCFG{ + valid: false, + err: nil, + validationErrors: []*asconfig.ValidationErr{}, + }, + logger: logrus.New(), + }, + wantErr: true, + }, + { + name: "neg2", + fields: fields{ + cfg: &mockCFG{ + valid: true, + err: fmt.Errorf("test_err"), + validationErrors: []*asconfig.ValidationErr{}, + }, + logger: logrus.New(), + }, + wantErr: true, + }, + { + name: "neg3", + fields: fields{ + cfg: &mockCFG{ + valid: true, + err: nil, + validationErrors: []*asconfig.ValidationErr{{}}, + }, + logger: logrus.New(), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ac := NewConfigValidator(tt.fields.cfg, tt.fields.managementLibLogger, tt.fields.aerospikeVersion) + if _, err := ac.Validate(); (err != nil) != tt.wantErr { + t.Errorf("asconf.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/conf/constants.go b/conf/constants.go new file mode 100644 index 0000000..9849cca --- /dev/null +++ b/conf/constants.go @@ -0,0 +1,13 @@ +package conf + +import "fmt" + +type Format string + +const ( + Invalid Format = "" + YAML Format = "yaml" + AsConfig Format = "asconfig" +) + +var ErrInvalidFormat = fmt.Errorf("invalid config format") diff --git a/asconf/utils.go b/conf/loader.go similarity index 67% rename from asconf/utils.go rename to conf/loader.go index 80ae068..d6e97ea 100644 --- a/asconf/utils.go +++ b/conf/loader.go @@ -1,49 +1,108 @@ -package asconf +package conf import ( + "bytes" "fmt" "sort" "strings" lib "github.com/aerospike/aerospike-management-lib" + "github.com/aerospike/aerospike-management-lib/asconfig" + "github.com/go-logr/logr" + "gopkg.in/yaml.v3" ) -// copied from the management lib asconfig package -var singularToPlural = map[string]string{ - "access-address": "access-addresses", - "address": "addresses", - "alternate-access-address": "alternate-access-addresses", - "datacenter": "datacenters", - "dc": "dcs", - "dc-int-ext-ipmap": "dc-int-ext-ipmap", - "dc-node-address-port": "dc-node-address-ports", - "device": "devices", - "file": "files", - "feature-key-file": "feature-key-files", - "mount": "mounts", - "http-url": "http-urls", - "ignore-bin": "ignore-bins", - "ignore-set": "ignore-sets", - "logging": "logging", - "mesh-seed-address-port": "mesh-seed-address-ports", - "multicast-group": "multicast-groups", - "namespace": "namespaces", - "node-address-port": "node-address-ports", - "report-data-op": "report-data-op", - "report-data-op-user": "report-data-op-user", - "report-data-op-role": "report-data-op-role", - "role-query-pattern": "role-query-patterns", - "set": "sets", - "ship-bin": "ship-bins", - "ship-set": "ship-sets", - "tls": "tls", - "tls-access-address": "tls-access-addresses", - "tls-address": "tls-addresses", - "tls-alternate-access-address": "tls-alternate-access-addresses", - "tls-mesh-seed-address-port": "tls-mesh-seed-address-ports", - "tls-node": "tls-nodes", - "xdr-remote-datacenter": "xdr-remote-datacenters", - "tls-authenticate-client": "tls-authenticate-client", +func NewASConfigFromBytes(log logr.Logger, src []byte, srcFmt Format) (*asconfig.AsConfig, error) { + var err error + var cfg *asconfig.AsConfig + + switch srcFmt { + case YAML: + cfg, err = loadYAML(log, src) + case AsConfig: + cfg, err = loadAsConf(log, src) + default: + return nil, fmt.Errorf("%w %s", ErrInvalidFormat, srcFmt) + } + + if err != nil { + return nil, err + } + + // recreate the management lib config + // with a sorted config map so that output + // is always in the same order + cmap := cfg.ToMap() + + mutateMap(*cmap, []mapping{ + sortLists, + }) + + cfg, err = asconfig.NewMapAsConfig( + log, + "", // TODO: Remove when management lib merges PR #41 + *cmap, + ) + + return cfg, err +} + +func loadYAML(log logr.Logger, src []byte) (*asconfig.AsConfig, error) { + + var data map[string]any + + err := yaml.Unmarshal(src, &data) + if err != nil { + return nil, err + } + + c, err := asconfig.NewMapAsConfig( + log, + "", // TODO: Remove when management lib merges PR #41 + data, + ) + + if err != nil { + return nil, fmt.Errorf("failed to initialize asconfig from yaml: %w", err) + } + + return c, nil +} + +func loadAsConf(log logr.Logger, src []byte) (*asconfig.AsConfig, error) { + + reader := bytes.NewReader(src) + + // TODO: Why doesn't the management lib do the map mutation? FromConfFile + // implies it does. + c, err := asconfig.FromConfFile(log, "", reader) // TODO: Remove "" when management lib merges PR #41 + if err != nil { + return nil, fmt.Errorf("failed to parse asconfig file: %w", err) + } + + // the aerospike management lib parses asconfig files into + // a format that its validation rejects + // this is because the schema files are meant to + // validate the aerospike kubernetes operator's asconfig yaml format + // so we modify the map here to match that format + cmap := *c.ToMap() + + mutateMap(cmap, []mapping{ + typedContextsToObject, + toPlural, + }) + + c, err = asconfig.NewMapAsConfig( + log, + "", // TODO: Remove when management lib merges PR #41 + cmap, + ) + + if err != nil { + return nil, err + } + + return c, nil } type configMap = lib.Stats @@ -73,85 +132,6 @@ func mutateMap(in configMap, funcs []mapping) { } } -/* -sortLists sorts slices of config sections by the "name" or "type" -key that the management lib adds to config list items -Ex config: -namespace ns2 {} -namespace ns1 {} --> -namespace ns1 {} -namespace ns2 {} - -Ex matching configMap - - configMap{ - "namespace": []configMap{ - configMap{ - "name": "ns2", - }, - configMap{ - "name": "ns1", - }, - } - } - --> - - configMap{ - "namespace": []configMap{ - configMap{ - "name": "ns1", - }, - configMap{ - "name": "ns2", - }, - } - } -*/ -func sortLists(k string, v any, m configMap) { - if v, ok := v.([]configMap); ok { - sort.Slice(v, func(i int, j int) bool { - iv, iok := v[i]["name"] - jv, jok := v[j]["name"] - - // sections may also use the "type" field to identify themselves - if !iok { - iv, iok = v[i]["type"] - } - - if !jok { - jv, jok = v[j]["type"] - } - - // if i or both don't have id fields, consider them i >= j - if !iok { - return false - } - - // if only j has an id field consider i < j - if !jok { - return true - } - - iname := iv.(string) - jname := jv.(string) - - gt := strings.Compare(iname, jname) - - switch gt { - case 1: - return true - case -1, 0: - return false - default: - panic("unexpected gt value") - } - }) - m[k] = v - } -} - /* typedContextsToObject converts config entries that the management lib parses as literal strings into the objects that the yaml schemas expect. @@ -186,6 +166,18 @@ func typedContextsToObject(k string, v any, m configMap) { } } +// isTypedContext returns true for asconfig contexts +// that can map to strings instead of contexts +func isTypedContext(in string) bool { + + switch in { + case "storage-engine", "index-type", "sindex-type": + return true + default: + return false + } +} + /* toPlural converts the keys that the management lib asconf parser parses as singular, to the plural keys that the yaml schemas expect @@ -209,7 +201,7 @@ func toPlural(k string, v any, m configMap) { // convert asconfig fields/contexts that need to be plural // in order to create valid asconfig yaml. - if plural, ok := singularToPlural[k]; ok { + if plural := asconfig.PluralOf(k); plural != k { // if the config item can be plural or singular and is not a slice // then the item should not be converted to the plural form. // If the management lib ever parses list entries as anything other @@ -247,29 +239,81 @@ func isListOrString(name string) bool { } } -// isTypedContext returns true for asconfig contexts -// that can map to strings instead of contexts -func isTypedContext(in string) bool { +/* +sortLists sorts slices of config sections by the "name" or "type" +key that the management lib adds to config list items +Ex config: +namespace ns2 {} +namespace ns1 {} +-> +namespace ns1 {} +namespace ns2 {} - switch in { - case "storage-engine", "index-type", "sindex-type": - return true - default: - return false +Ex matching configMap + + configMap{ + "namespace": []configMap{ + configMap{ + "name": "ns2", + }, + configMap{ + "name": "ns1", + }, + } } -} -func ParseFmtString(in string) (f Format, err error) { +-> - switch strings.ToLower(in) { - case "yaml", "yml": - f = YAML - case "asconfig", "conf", "asconf": - f = AsConfig - default: - f = Invalid - err = fmt.Errorf("%w: %s", ErrInvalidFormat, in) + configMap{ + "namespace": []configMap{ + configMap{ + "name": "ns1", + }, + configMap{ + "name": "ns2", + }, + } } +*/ +func sortLists(k string, v any, m configMap) { + if v, ok := v.([]configMap); ok { + sort.Slice(v, func(i int, j int) bool { + iv, iok := v[i]["name"] + jv, jok := v[j]["name"] + + // sections may also use the "type" field to identify themselves + if !iok { + iv, iok = v[i]["type"] + } - return + if !jok { + jv, jok = v[j]["type"] + } + + // if i or both don't have id fields, consider them i >= j + if !iok { + return false + } + + // if only j has an id field consider i < j + if !jok { + return true + } + + iname := iv.(string) + jname := jv.(string) + + gt := strings.Compare(iname, jname) + + switch gt { + case 1: + return true + case -1, 0: + return false + default: + panic("unexpected gt value") + } + }) + m[k] = v + } } diff --git a/asconf/metadata/metadata.go b/conf/metadata/metadata.go similarity index 100% rename from asconf/metadata/metadata.go rename to conf/metadata/metadata.go diff --git a/asconf/metadata/metadata_test.go b/conf/metadata/metadata_test.go similarity index 97% rename from asconf/metadata/metadata_test.go rename to conf/metadata/metadata_test.go index 8ceb001..9df2aab 100644 --- a/asconf/metadata/metadata_test.go +++ b/conf/metadata/metadata_test.go @@ -1,13 +1,10 @@ -//go:build unit -// +build unit - package metadata_test import ( "reflect" "testing" - "github.com/aerospike/asconfig/asconf/metadata" + "github.com/aerospike/asconfig/conf/metadata" ) var testBasic = ` diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..490c4f5 --- /dev/null +++ b/constants.go @@ -0,0 +1,9 @@ +package main + +type Format string + +const ( + Invalid Format = "" + YAML Format = "yaml" + AsConfig Format = "asconfig" +) diff --git a/go.mod b/go.mod index 6329a69..5eeef5d 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ module github.com/aerospike/asconfig go 1.20 require ( - github.com/aerospike/aerospike-client-go/v6 v6.14.0 - github.com/aerospike/aerospike-management-lib v0.0.0-20231207005705-6535f64a52e3 + github.com/aerospike/aerospike-client-go/v6 v6.14.1 + github.com/aerospike/aerospike-management-lib v0.0.0-20231229132959-08273f7a41b7 + github.com/aerospike/tools-common-go v0.0.0-20240104200544-8ced38228ab3 github.com/bombsimon/logrusr/v4 v4.0.0 github.com/docker/docker v24.0.7+incompatible github.com/go-logr/logr v1.2.4 @@ -35,12 +36,12 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect - golang.org/x/mod v0.9.0 // indirect + golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.7.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.54.0 // indirect diff --git a/go.sum b/go.sum index 2ed0709..690f153 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,22 @@ 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-client-go/v6 v6.14.0 h1:Z3FcGWJda1sagzdc6Akz4EJ13Pq55Uyn6qtFLrVUDd0= -github.com/aerospike/aerospike-client-go/v6 v6.14.0/go.mod h1:/0Wm81GhMqem+9flWcpazPKoRfjFeG6WrQdXGiMNi0A= +github.com/aerospike/aerospike-client-go/v6 v6.14.1 h1:1DB9rgbPcCSjR7QS+2CL4MM4atdVcRiWa2AVKO7ydyY= +github.com/aerospike/aerospike-client-go/v6 v6.14.1/go.mod h1:/0Wm81GhMqem+9flWcpazPKoRfjFeG6WrQdXGiMNi0A= github.com/aerospike/aerospike-management-lib v0.0.0-20231207005705-6535f64a52e3 h1:Qzcgl+U+Ffhgff7lQb/2gNeYbk3zTgU4hgfeEprsWG8= github.com/aerospike/aerospike-management-lib v0.0.0-20231207005705-6535f64a52e3/go.mod h1:C/Pott1bSdCnd4mcaa+l/8+Kq77ypMqlZmVq1tAegl8= +github.com/aerospike/aerospike-management-lib v0.0.0-20231221011837-c8471d4a13c7 h1:ouH7fq84xDQ7tet0sj3J9iNAR71GUaH0WgNYytqwbbk= +github.com/aerospike/aerospike-management-lib v0.0.0-20231221011837-c8471d4a13c7/go.mod h1:54II0rXTkJ6Pi7MTR7bCSlQXPCdHr/kqR3aUuKht6sY= +github.com/aerospike/aerospike-management-lib v0.0.0-20231229132959-08273f7a41b7 h1:D+/s/d6l26SRACLJlegmgE35o0qWBjLcYcuB14UhnDc= +github.com/aerospike/aerospike-management-lib v0.0.0-20231229132959-08273f7a41b7/go.mod h1:54II0rXTkJ6Pi7MTR7bCSlQXPCdHr/kqR3aUuKht6sY= +github.com/aerospike/tools-common-go v0.0.0-20231222235920-09ca1b88987b h1:Bg96vYjtO2ASocHkXzWKpePPMRjhyhkVSPh9RMp30E8= +github.com/aerospike/tools-common-go v0.0.0-20231222235920-09ca1b88987b/go.mod h1:JaDR4z9G/GsYx7N33UBmKY0Bm0QlKpCzX23Kub1FOVo= +github.com/aerospike/tools-common-go v0.0.0-20240104005705-7490e2c8d4c8 h1:TKMHsLsuRn+zc4ulzuy0bm5huyQU0PfcGbAEiSRQyxc= +github.com/aerospike/tools-common-go v0.0.0-20240104005705-7490e2c8d4c8/go.mod h1:JaDR4z9G/GsYx7N33UBmKY0Bm0QlKpCzX23Kub1FOVo= +github.com/aerospike/tools-common-go v0.0.0-20240104165305-9765b3e6027a h1:DNBIm/9z58/PkfExVuH9t7gWEdsYeemMD5Ey3dxEr5s= +github.com/aerospike/tools-common-go v0.0.0-20240104165305-9765b3e6027a/go.mod h1:JaDR4z9G/GsYx7N33UBmKY0Bm0QlKpCzX23Kub1FOVo= +github.com/aerospike/tools-common-go v0.0.0-20240104200544-8ced38228ab3 h1:9+yCy6V+ab/iFQOciw8GicDx1fXBIUnT2KmDKh1ulbI= +github.com/aerospike/tools-common-go v0.0.0-20240104200544-8ced38228ab3/go.mod h1:JaDR4z9G/GsYx7N33UBmKY0Bm0QlKpCzX23Kub1FOVo= github.com/bombsimon/logrusr/v4 v4.0.0 h1:Pm0InGphX0wMhPqC02t31onlq9OVyJ98eP/Vh63t1Oo= github.com/bombsimon/logrusr/v4 v4.0.0/go.mod h1:pjfHC5e59CvjTBIU3V3sGhFWFAnsnhOR03TRc6im0l8= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -95,6 +107,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -118,6 +131,7 @@ golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/integration_test.go b/integration_test.go index c137341..91dc7f3 100644 --- a/integration_test.go +++ b/integration_test.go @@ -1,6 +1,3 @@ -//go:build integration -// +build integration - package main import ( @@ -20,8 +17,9 @@ import ( "testing" "time" - "github.com/aerospike/asconfig/asconf/metadata" + mgmtLib "github.com/aerospike/aerospike-management-lib" "github.com/aerospike/asconfig/cmd" + "github.com/aerospike/asconfig/conf/metadata" "github.com/aerospike/asconfig/testutils" "github.com/docker/docker/api/types" @@ -228,28 +226,6 @@ var testFiles = []testutils.TestData{ }, } -func versionLessThanEqual(l string, r string) bool { - var majorLess, minorLess, patchLess bool - ltok := strings.Split(l, ".") - rtok := strings.Split(r, ".") - - lmaj, _ := strconv.Atoi(ltok[0]) - lmin, _ := strconv.Atoi(ltok[1]) - lpat, _ := strconv.Atoi(ltok[2]) - - rmaj, _ := strconv.Atoi(rtok[0]) - rmin, _ := strconv.Atoi(rtok[1]) - rpat, _ := strconv.Atoi(rtok[2]) - - majorLess = lmaj <= rmaj - minorLess = lmin <= rmin - patchLess = lpat <= rpat - - return (majorLess && minorLess && patchLess) || - (majorLess && minorLess && !patchLess) || - (majorLess && !minorLess && !patchLess) -} - func getVersion(l []string) (v string) { i := testutils.IndexOf(l, "-a") if i >= 0 { @@ -334,7 +310,12 @@ func TestYamlToConf(t *testing.T) { // test that the converted config works with an Aerospike server if !tf.SkipServerTest { version := getVersion(tf.Arguments) - runServer(version, confPath, dockerClient, t, tf) + id := runServer(version, confPath, dockerClient, t, tf) + + time.Sleep(time.Second * 3) // need this to allow logs to accumulate + + stopServer(id, dockerClient) + checkContainerLogs(id, t, tf, tmpServerLogPath) } // cleanup the destination file @@ -400,7 +381,7 @@ func getDockerAuthFromEnv(auth testutils.DockerAuth) (string, error) { return authStr, nil } -func runServer(version string, confPath string, dockerClient *client.Client, t *testing.T, td testutils.TestData) { +func runServer(version string, confPath string, dockerClient *client.Client, t *testing.T, td testutils.TestData) string { containerName := "aerospike:" + version if td.ServerImage != "" { containerName = td.ServerImage @@ -437,7 +418,10 @@ func runServer(version string, confPath string, dockerClient *client.Client, t * featureKeyPath := filepath.Join(featKeyDir, "featuresv2.conf") lastServerWithFeatureKeyVersion1 := "5.3.0" - if versionLessThanEqual(strings.TrimPrefix(version, "ee-"), lastServerWithFeatureKeyVersion1) { + + if val, err := mgmtLib.CompareVersions(strings.TrimPrefix(version, "ee-"), lastServerWithFeatureKeyVersion1); err != nil { + t.Error(err) + } else if val <= 0 { featureKeyPath = filepath.Join(featKeyDir, "featuresv1.conf") } @@ -487,26 +471,23 @@ func runServer(version string, confPath string, dockerClient *client.Client, t * t.Error(err) } - // cleanup container - defer func() { - err = testutils.RemoveAerospikeContainer(id, dockerClient) - if err != nil { - t.Error(err) - } - }() - err = testutils.StartAerospikeContainer(id, dockerClient) if err != nil { t.Error(err) } + return id + // need this to allow logs to accumulate time.Sleep(time.Second * 3) - err = testutils.StopAerospikeContainer(id, dockerClient) +} + +func stopServer(id string, dockerClient *client.Client) error { + err := testutils.StopAerospikeContainer(id, dockerClient) if err != nil { - t.Error(err) + return err } // time for Aerospike to close @@ -518,57 +499,17 @@ func runServer(version string, confPath string, dockerClient *client.Client, t * select { case err := <-errCh: if err != nil { - t.Error(err) + return err // TODO: Check if I need to do something to shutdown the channels. } case <-statusCh: } - data, err := docker("logs", id) + err = testutils.RemoveAerospikeContainer(id, dockerClient) if err != nil { - t.Error(err) + return err } - var containerOut string - containerOut = string(data) - - // containerOut := string(logs) - // if the server container logs are empty - // the server may have been configured to log to - // /var/log/aerospike/aerospike.log which is mapped - // to absTmpLog - if len(containerOut) == 0 { - data, err := os.ReadFile(absTmpLog) - if err != nil { - t.Error(err) - } - containerOut = string(data) - } - - // if the logs are still empty, the server logged somewhere else - // or there is a problem, fail in this case - if len(containerOut) == 0 { - t.Errorf("suite: %+v\nAerospike container logs are empty", td) - } - - // some tests use aerospike versions from when no enterprise container was published - // these will fail with "'x feature' is enterprise-only" - // always ignore this failure - td.ServerErrorAllowList = append(td.ServerErrorAllowList, "' is enterprise-only") - - reg := regexp.MustCompile(`CRITICAL \(config\):.*`) - configErrors := reg.FindAllString(containerOut, -1) - for _, cfgErr := range configErrors { - exempted := false - for _, exemption := range td.ServerErrorAllowList { - if strings.Contains(cfgErr, exemption) { - exempted = true - } - } - - if !exempted { - t.Errorf("suite: %+v\nAerospike encountered a configuration error...\n%s", td, containerOut) - } - } + return nil } var confToYamlTests = []testutils.TestData{ @@ -773,7 +714,12 @@ func TestConfToYaml(t *testing.T) { // test that the converted config works with an Aerospike server if !tf.SkipServerTest { version := getVersion(tf.Arguments) - runServer(version, finalConfPath, dockerClient, t, tf) + id := runServer(version, finalConfPath, dockerClient, t, tf) + + time.Sleep(3) // need this to allow logs to accumulate + + stopServer(id, dockerClient) + checkContainerLogs(id, t, tf, tmpServerLogPath) } // cleanup the destination files @@ -796,6 +742,59 @@ func docker(args ...string) ([]byte, error) { return out, err } +func getContainerLogs(id string) ([]byte, error) { + return docker("logs", id) +} + +func checkContainerLogs(id string, t *testing.T, td testutils.TestData, absTmpLog string) { + data, err := getContainerLogs(id) + if err != nil { + t.Error(err) + } + + var containerOut string + containerOut = string(data) + + // containerOut := string(logs) + // if the server container logs are empty + // the server may have been configured to log to + // /var/log/aerospike/aerospike.log which is mapped + // to absTmpLog + if len(containerOut) == 0 { + data, err := os.ReadFile(absTmpLog) + if err != nil { + t.Error(err) + } + containerOut = string(data) + } + + // if the logs are still empty, the server logged somewhere else + // or there is a problem, fail in this case + if len(containerOut) == 0 { + t.Errorf("suite: %+v\nAerospike container logs are empty", td) + } + + // some tests use aerospike versions from when no enterprise container was published + // these will fail with "'x feature' is enterprise-only" + // always ignore this failure + td.ServerErrorAllowList = append(td.ServerErrorAllowList, "' is enterprise-only") + + reg := regexp.MustCompile(`CRITICAL \(config\):.*`) + configErrors := reg.FindAllString(containerOut, -1) + for _, cfgErr := range configErrors { + exempted := false + for _, exemption := range td.ServerErrorAllowList { + if strings.Contains(cfgErr, exemption) { + exempted = true + } + } + + if !exempted { + t.Errorf("suite: %+v\nAerospike encountered a configuration error...\n%s", td, containerOut) + } + } +} + func diff(args ...string) ([]byte, error) { args = append([]string{"diff"}, args...) com := exec.Command(binPath+"/asconfig.test", args...)