github.com/xgoffin/jenkins-library@v1.154.0/cmd/abapEnvironmentBuild.go (about)

     1  package cmd
     2  
     3  import (
     4  	"encoding/json"
     5  	"reflect"
     6  	"strings"
     7  	"time"
     8  
     9  	abapbuild "github.com/SAP/jenkins-library/pkg/abap/build"
    10  	"github.com/SAP/jenkins-library/pkg/abaputils"
    11  	"github.com/SAP/jenkins-library/pkg/command"
    12  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    13  	"github.com/SAP/jenkins-library/pkg/log"
    14  	"github.com/SAP/jenkins-library/pkg/piperutils"
    15  	"github.com/SAP/jenkins-library/pkg/telemetry"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  type abapEnvironmentBuildUtils interface {
    20  	command.ExecRunner
    21  	abaputils.Communication
    22  	abapbuild.Publish
    23  	abapbuild.HTTPSendLoader
    24  	getMaxRuntime() time.Duration
    25  	getPollingInterval() time.Duration
    26  	publish()
    27  }
    28  
    29  type abapEnvironmentBuildUtilsBundle struct {
    30  	*command.Command
    31  	*piperhttp.Client
    32  	*abaputils.AbapUtils
    33  	maxRuntime      time.Duration
    34  	pollingInterval time.Duration
    35  	storePublish    publish
    36  }
    37  
    38  type publish struct {
    39  	stepName  string
    40  	workspace string
    41  	reports   []piperutils.Path
    42  	links     []piperutils.Path
    43  }
    44  
    45  func (p *publish) publish() {
    46  	if p.stepName != "" {
    47  		abapbuild.PersistReportsAndLinks(p.stepName, p.workspace, p.reports, p.links)
    48  	}
    49  }
    50  
    51  func (aEBUB *abapEnvironmentBuildUtilsBundle) publish() {
    52  	aEBUB.storePublish.publish()
    53  }
    54  
    55  func (aEBUB *abapEnvironmentBuildUtilsBundle) getMaxRuntime() time.Duration {
    56  	return aEBUB.maxRuntime
    57  }
    58  
    59  func (aEBUB *abapEnvironmentBuildUtilsBundle) getPollingInterval() time.Duration {
    60  	return aEBUB.pollingInterval
    61  }
    62  
    63  func (aEBUB *abapEnvironmentBuildUtilsBundle) PersistReportsAndLinks(stepName, workspace string, reports, links []piperutils.Path) {
    64  	//abapbuild.PersistReportsAndLinks(stepName, workspace, reports, links)
    65  	if aEBUB.storePublish.stepName == "" {
    66  		aEBUB.storePublish.stepName = stepName
    67  		aEBUB.storePublish.workspace = workspace
    68  		aEBUB.storePublish.reports = reports
    69  		aEBUB.storePublish.links = links
    70  	} else {
    71  		aEBUB.storePublish.reports = append(aEBUB.storePublish.reports, reports...)
    72  		aEBUB.storePublish.links = append(aEBUB.storePublish.reports, links...)
    73  	}
    74  }
    75  
    76  func newAbapEnvironmentBuildUtils(maxRuntime time.Duration, pollingInterval time.Duration) abapEnvironmentBuildUtils {
    77  	utils := abapEnvironmentBuildUtilsBundle{
    78  		Command: &command.Command{},
    79  		Client:  &piperhttp.Client{},
    80  		AbapUtils: &abaputils.AbapUtils{
    81  			Exec: &command.Command{},
    82  		},
    83  		maxRuntime:      maxRuntime * time.Minute,
    84  		pollingInterval: pollingInterval * time.Second,
    85  		storePublish:    publish{},
    86  	}
    87  	// Reroute command output to logging framework
    88  	utils.Stdout(log.Writer())
    89  	utils.Stderr(log.Writer())
    90  	return &utils
    91  }
    92  
    93  func abapEnvironmentBuild(config abapEnvironmentBuildOptions, telemetryData *telemetry.CustomData, cpe *abapEnvironmentBuildCommonPipelineEnvironment) {
    94  	utils := newAbapEnvironmentBuildUtils(time.Duration(config.MaxRuntimeInMinutes), time.Duration(config.PollingIntervalInSeconds))
    95  	if err := runAbapEnvironmentBuild(&config, telemetryData, &utils, cpe); err != nil {
    96  		log.Entry().WithError(err).Fatal("step execution failed")
    97  	}
    98  }
    99  
   100  func runAbapEnvironmentBuild(config *abapEnvironmentBuildOptions, telemetryData *telemetry.CustomData, utils *abapEnvironmentBuildUtils, cpe *abapEnvironmentBuildCommonPipelineEnvironment) error {
   101  
   102  	conn := new(abapbuild.Connector)
   103  	if err := initConnection(conn, config, utils); err != nil {
   104  		return errors.Wrap(err, "Connector initialization for communication with the ABAP system failed")
   105  	}
   106  
   107  	valuesList, err := evaluateAddonDescriptor(config)
   108  	if err != nil {
   109  		return errors.Wrap(err, "Error during the evaluation of the AddonDescriptor")
   110  	}
   111  
   112  	finalValues, err := runBuilds(conn, config, utils, valuesList)
   113  	//files should be published, even if an error occured
   114  	(*utils).publish()
   115  	if err != nil {
   116  		return err
   117  	}
   118  
   119  	cpe.abap.buildValues, err = convertValuesForCPE(finalValues)
   120  	if err != nil {
   121  		return errors.Wrap(err, "Error during the conversion of the values for the commonPipelineenvironment")
   122  	}
   123  	return nil
   124  }
   125  
   126  func runBuilds(conn *abapbuild.Connector, config *abapEnvironmentBuildOptions, utils *abapEnvironmentBuildUtils, valuesList [][]abapbuild.Value) ([]abapbuild.Value, error) {
   127  	var finalValues []abapbuild.Value
   128  	//No addonDescriptor involved
   129  	if len(valuesList) == 0 {
   130  		values, err := generateValuesOnlyFromConfig(config)
   131  		if err != nil {
   132  			return finalValues, errors.Wrap(err, "Generating the values from config failed")
   133  		}
   134  		finalValues, err = runBuild(conn, config, utils, values)
   135  		if err != nil {
   136  			return finalValues, errors.Wrap(err, "Error during execution of build framework")
   137  		}
   138  	} else {
   139  		//Run several times for each repository in the addonDescriptor
   140  		var errstrings []string
   141  		vE := valuesEvaluator{}
   142  		vE.m = make(map[string]string)
   143  		for _, values := range valuesList {
   144  			cummulatedValues, err := generateValuesWithAddonDescriptor(config, values)
   145  			if err != nil {
   146  				return finalValues, errors.Wrap(err, "Error generating input values")
   147  			}
   148  			finalValuesForOneBuild, err := runBuild(conn, config, utils, cummulatedValues)
   149  			if err != nil {
   150  				err = errors.Wrapf(err, "Build with input values %s failed", values)
   151  				if config.StopOnFirstError {
   152  					return finalValues, err
   153  				}
   154  				errstrings = append(errstrings, err.Error())
   155  			}
   156  			finalValuesForOneBuild = removeAddonDescriptorValues(finalValuesForOneBuild, values)
   157  			//This means: probably values are duplicated, but the first one wins -> perhaps change this in the future if needed
   158  			vE.appendValuesIfNotPresent(finalValuesForOneBuild, false)
   159  		}
   160  		finalValues = vE.generateValueSlice()
   161  		if len(errstrings) > 0 {
   162  			finalError := errors.Errorf("%d out %d build runs failed:\n%s", len(errstrings), len(valuesList), (strings.Join(errstrings, "\n")))
   163  			return finalValues, finalError
   164  		}
   165  	}
   166  	return finalValues, nil
   167  }
   168  
   169  func initConnection(conn *abapbuild.Connector, config *abapEnvironmentBuildOptions, utils *abapEnvironmentBuildUtils) error {
   170  	var connConfig abapbuild.ConnectorConfiguration
   171  	connConfig.CfAPIEndpoint = config.CfAPIEndpoint
   172  	connConfig.CfOrg = config.CfOrg
   173  	connConfig.CfSpace = config.CfSpace
   174  	connConfig.CfServiceInstance = config.CfServiceInstance
   175  	connConfig.CfServiceKeyName = config.CfServiceKeyName
   176  	connConfig.Host = config.Host
   177  	connConfig.Username = config.Username
   178  	connConfig.Password = config.Password
   179  	connConfig.MaxRuntimeInMinutes = config.MaxRuntimeInMinutes
   180  	connConfig.CertificateNames = config.CertificateNames
   181  
   182  	if err := conn.InitBuildFramework(connConfig, *utils, *utils); err != nil {
   183  		return err
   184  	}
   185  
   186  	conn.MaxRuntime = (*utils).getMaxRuntime()
   187  	conn.PollingInterval = (*utils).getPollingInterval()
   188  	return nil
   189  }
   190  
   191  // ***********************************Run Build***************************************************************
   192  func runBuild(conn *abapbuild.Connector, config *abapEnvironmentBuildOptions, utils *abapEnvironmentBuildUtils, values []abapbuild.Value) ([]abapbuild.Value, error) {
   193  	var finalValues []abapbuild.Value
   194  	var inputValues abapbuild.Values
   195  	inputValues.Values = values
   196  
   197  	build := myBuild{
   198  		Build: abapbuild.Build{
   199  			Connector: *conn,
   200  		},
   201  		abapEnvironmentBuildOptions: config,
   202  	}
   203  	if err := build.Start(inputValues); err != nil {
   204  		return finalValues, err
   205  	}
   206  
   207  	if err := build.Poll(); err != nil {
   208  		return finalValues, errors.Wrap(err, "Error during the polling for the final state of the build run")
   209  	}
   210  
   211  	if err := build.PrintLogs(); err != nil {
   212  		return finalValues, errors.Wrap(err, "Error printing the logs")
   213  	}
   214  
   215  	errBuildRun := build.EvaluteIfBuildSuccessful()
   216  
   217  	if err := build.Download(); err != nil {
   218  		if errBuildRun != nil {
   219  			errWraped := errors.Errorf("Download failed after execution of build failed: %v. Build error: %v", err, errBuildRun)
   220  			return finalValues, errWraped
   221  		}
   222  		return finalValues, err
   223  	}
   224  	if err := build.Publish(utils); err != nil {
   225  		return finalValues, err
   226  	}
   227  
   228  	finalValues, err := build.GetFinalValues()
   229  	if err != nil {
   230  		return finalValues, err
   231  	}
   232  	return finalValues, errBuildRun
   233  }
   234  
   235  type myBuild struct {
   236  	abapbuild.Build
   237  	*abapEnvironmentBuildOptions
   238  }
   239  
   240  func (b *myBuild) Start(values abapbuild.Values) error {
   241  	if err := b.Build.Start(b.abapEnvironmentBuildOptions.Phase, values); err != nil {
   242  		return errors.Wrap(err, "Error starting the build framework")
   243  	}
   244  	return nil
   245  }
   246  
   247  func (b *myBuild) EvaluteIfBuildSuccessful() error {
   248  	if err := b.Build.EvaluteIfBuildSuccessful(b.TreatWarningsAsError); err != nil {
   249  		return errors.Wrap(err, "Build ended without success")
   250  	}
   251  	return nil
   252  }
   253  
   254  func (b *myBuild) Download() error {
   255  	if b.DownloadAllResultFiles {
   256  		if err := b.DownloadAllResults(b.SubDirectoryForDownload, b.FilenamePrefixForDownload); err != nil {
   257  			return errors.Wrap(err, "Error during the download of the result files")
   258  		}
   259  	} else {
   260  		if err := b.DownloadResults(b.DownloadResultFilenames, b.SubDirectoryForDownload, b.FilenamePrefixForDownload); err != nil {
   261  			return errors.Wrapf(err, "Error during the download of the result files %s", b.DownloadResultFilenames)
   262  		}
   263  	}
   264  	return nil
   265  }
   266  
   267  func (b *myBuild) Publish(utils *abapEnvironmentBuildUtils) error {
   268  	if b.PublishAllDownloadedResultFiles {
   269  		b.PublishAllDownloadedResults("abapEnvironmentBuild", *utils)
   270  	} else {
   271  		if err := b.PublishDownloadedResults("abapEnvironmentBuild", b.PublishResultFilenames, *utils); err != nil {
   272  			return errors.Wrapf(err, "Error during the publish of the result files %s", b.PublishResultFilenames)
   273  		}
   274  	}
   275  	return nil
   276  }
   277  
   278  func (b *myBuild) GetFinalValues() ([]abapbuild.Value, error) {
   279  	var values []abapbuild.Value
   280  	if err := b.GetValues(); err != nil {
   281  		return values, errors.Wrapf(err, "Error getting the values from build framework")
   282  	}
   283  	return b.Build.Values, nil
   284  }
   285  
   286  // **********************************Values Handling**************************************************************
   287  func convertValuesForCPE(values []abapbuild.Value) (string, error) {
   288  	type cpeValue struct {
   289  		ValueID string `json:"value_id"`
   290  		Value   string `json:"value"`
   291  	}
   292  	var cpeValues []cpeValue
   293  	byt, err := json.Marshal(&values)
   294  	if err != nil {
   295  		return "", errors.Wrapf(err, "Error converting the values from the build framework")
   296  	}
   297  	if err := json.Unmarshal(byt, &cpeValues); err != nil {
   298  		return "", errors.Wrapf(err, "Error converting the values from the build framework into the structure for the commonPipelineEnvironment")
   299  	}
   300  	jsonBytes, err := json.Marshal(cpeValues)
   301  	if err != nil {
   302  		return "", errors.Wrapf(err, "Error converting the converted values")
   303  	}
   304  	return string(jsonBytes), nil
   305  }
   306  
   307  func removeAddonDescriptorValues(finalValuesFromBuild []abapbuild.Value, valuesFromAddonDescriptor []abapbuild.Value) []abapbuild.Value {
   308  	var finalValues []abapbuild.Value
   309  	mapForAddonDescriptorValues := make(map[string]string)
   310  	for _, value := range valuesFromAddonDescriptor {
   311  		mapForAddonDescriptorValues[value.ValueID] = value.Value
   312  	}
   313  	for _, value := range finalValuesFromBuild {
   314  		_, present := mapForAddonDescriptorValues[value.ValueID]
   315  		if !present {
   316  			finalValues = append(finalValues, value)
   317  		}
   318  	}
   319  	return finalValues
   320  }
   321  
   322  func generateValuesWithAddonDescriptor(config *abapEnvironmentBuildOptions, repoValues []abapbuild.Value) ([]abapbuild.Value, error) {
   323  	var values []abapbuild.Value
   324  	vE := valuesEvaluator{}
   325  	//values from config
   326  	if err := vE.initialize(config.Values); err != nil {
   327  		return values, err
   328  	}
   329  	//values from addondescriptor
   330  	if err := vE.appendValuesIfNotPresent(repoValues, true); err != nil {
   331  		return values, err
   332  	}
   333  	//values from commonepipelineEnvironment
   334  	if err := vE.appendStringValuesIfNotPresent(config.CpeValues, false); err != nil {
   335  		return values, err
   336  	}
   337  	values = vE.generateValueSlice()
   338  	return values, nil
   339  }
   340  
   341  func generateValuesOnlyFromConfig(config *abapEnvironmentBuildOptions) ([]abapbuild.Value, error) {
   342  	return generateValuesWithAddonDescriptor(config, []abapbuild.Value{})
   343  }
   344  
   345  func generateValuesFromString(stringValues string) ([]abapbuild.Value, error) {
   346  	var values []abapbuild.Value
   347  	if len(stringValues) > 0 {
   348  		if err := json.Unmarshal([]byte(stringValues), &values); err != nil {
   349  			log.SetErrorCategory(log.ErrorConfiguration)
   350  			return values, errors.Wrapf(err, "Could not convert the values %s", stringValues)
   351  		}
   352  	}
   353  	return values, nil
   354  }
   355  
   356  type valuesEvaluator struct {
   357  	m map[string]string
   358  }
   359  
   360  func (vE *valuesEvaluator) initialize(stringValues string) error {
   361  	values, err := generateValuesFromString(stringValues)
   362  	if err != nil {
   363  		return errors.Wrapf(err, "Error converting the vales from the config")
   364  	}
   365  	vE.m = make(map[string]string)
   366  	for _, value := range values {
   367  		if (len(value.ValueID) == 0) || (len(value.Value) == 0) {
   368  			log.SetErrorCategory(log.ErrorConfiguration)
   369  			return errors.Errorf("Values %s from config have not the right format", stringValues)
   370  		}
   371  		_, present := vE.m[value.ValueID]
   372  		if present {
   373  			log.SetErrorCategory(log.ErrorConfiguration)
   374  			return errors.Errorf("Value_id %s is not unique in the config", value.ValueID)
   375  		}
   376  		vE.m[value.ValueID] = value.Value
   377  	}
   378  	return nil
   379  }
   380  
   381  func (vE *valuesEvaluator) appendStringValuesIfNotPresent(stringValues string, throwErrorIfPresent bool) error {
   382  	var values []abapbuild.Value
   383  	values, err := generateValuesFromString(stringValues)
   384  	if err != nil {
   385  		errors.Wrapf(err, "Error converting the vales from the commonPipelineEnvironment")
   386  	}
   387  	if err := vE.appendValuesIfNotPresent(values, throwErrorIfPresent); err != nil {
   388  		return err
   389  	}
   390  	return nil
   391  }
   392  
   393  func (vE *valuesEvaluator) appendValuesIfNotPresent(values []abapbuild.Value, throwErrorIfPresent bool) error {
   394  	for _, value := range values {
   395  		_, present := vE.m[value.ValueID]
   396  		if present || (value.ValueID == "PHASE") {
   397  			if throwErrorIfPresent {
   398  				return errors.Errorf("Value_id %s already existed in the config", value.ValueID)
   399  			}
   400  			log.Entry().Infof("Value %s already existed -> discard this value", value)
   401  		} else {
   402  			vE.m[value.ValueID] = value.Value
   403  		}
   404  	}
   405  	return nil
   406  }
   407  
   408  func (vE *valuesEvaluator) generateValueSlice() []abapbuild.Value {
   409  	var values []abapbuild.Value
   410  	var value abapbuild.Value
   411  	for k, v := range vE.m {
   412  		value.ValueID = k
   413  		value.Value = v
   414  		values = append(values, value)
   415  	}
   416  	return values
   417  }
   418  
   419  //**********************************Evaluate AddonDescriptor**************************************************************
   420  type myRepo struct {
   421  	abaputils.Repository
   422  }
   423  
   424  type condition struct {
   425  	Field    string `json:"field"`
   426  	Operator string `json:"operator"`
   427  	Value    string `json:"value"`
   428  }
   429  
   430  type useField struct {
   431  	Use    string `json:"use"`
   432  	Rename string `json:"renameTo"`
   433  }
   434  
   435  func evaluateAddonDescriptor(config *abapEnvironmentBuildOptions) ([][]abapbuild.Value, error) {
   436  	var listOfValuesList [][]abapbuild.Value
   437  	if len(config.AddonDescriptor) == 0 && len(config.UseFieldsOfAddonDescriptor) > 0 {
   438  		return listOfValuesList, errors.New("Config contains UseFieldsOfAddonDescriptor but no addonDescriptor is provided in the commonPipelineEnvironment")
   439  	}
   440  	if len(config.AddonDescriptor) > 0 {
   441  		addonDescriptor := new(abaputils.AddonDescriptor)
   442  		if err := addonDescriptor.InitFromJSONstring(config.AddonDescriptor); err != nil {
   443  			log.SetErrorCategory(log.ErrorConfiguration)
   444  			return listOfValuesList, errors.Wrap(err, "Error during the conversion of the AddonDescriptor")
   445  		}
   446  		for _, repo := range addonDescriptor.Repositories {
   447  			myRepo := myRepo{
   448  				Repository: repo,
   449  			}
   450  			use, err := myRepo.checkCondition(config)
   451  			if err != nil {
   452  				return listOfValuesList, errors.Wrapf(err, "Checking of ConditionOnAddonDescriptor failed")
   453  			}
   454  			if use {
   455  				values, err := myRepo.generateValues(config)
   456  				if err != nil {
   457  					return listOfValuesList, errors.Wrap(err, "Error generating values from AddonDescriptor")
   458  				}
   459  				if len(values) > 0 {
   460  					listOfValuesList = append(listOfValuesList, values)
   461  				}
   462  			}
   463  		}
   464  	}
   465  	return listOfValuesList, nil
   466  }
   467  
   468  func (mR *myRepo) checkCondition(config *abapEnvironmentBuildOptions) (bool, error) {
   469  	var conditions []condition
   470  	if len(config.ConditionOnAddonDescriptor) > 0 {
   471  		if err := json.Unmarshal([]byte(config.ConditionOnAddonDescriptor), &conditions); err != nil {
   472  			log.SetErrorCategory(log.ErrorConfiguration)
   473  			return false, errors.Wrapf(err, "Conversion of ConditionOnAddonDescriptor in the config failed")
   474  		}
   475  		for _, cond := range conditions {
   476  			if cond.Field == "" || cond.Operator == "" || cond.Value == "" {
   477  				log.SetErrorCategory(log.ErrorConfiguration)
   478  				return false, errors.Errorf("Invalid condition for field %s with operator %s and value %s", cond.Field, cond.Operator, cond.Value)
   479  			}
   480  			use, err := mR.amI(cond.Field, cond.Operator, cond.Value)
   481  			if err != nil {
   482  				return false, errors.Wrapf(err, "Checking the field %s failed", cond.Field)
   483  			}
   484  			if !use {
   485  				log.Entry().Infof("addonDescriptor with the name %s does not fulfil the requierement %s%s%s from the ConditionOnAddonDescriptor, therefore it is not used", mR.Name, cond.Field, cond.Operator, cond.Value)
   486  				return false, nil
   487  			}
   488  			log.Entry().Infof("addonDescriptor with the name %s does fulfil the requierement %s%s%s in the ConditionOnAddonDescriptor", mR.Name, cond.Field, cond.Operator, cond.Value)
   489  		}
   490  	}
   491  	return true, nil
   492  }
   493  
   494  func (mR *myRepo) generateValues(config *abapEnvironmentBuildOptions) ([]abapbuild.Value, error) {
   495  	var values []abapbuild.Value
   496  	var useFields []useField
   497  	if len(config.UseFieldsOfAddonDescriptor) == 0 {
   498  		log.Entry().Infof("UseFieldsOfAddonDescriptor is empty, nothing is used from the addonDescriptor")
   499  	} else {
   500  		if err := json.Unmarshal([]byte(config.UseFieldsOfAddonDescriptor), &useFields); err != nil {
   501  			log.SetErrorCategory(log.ErrorConfiguration)
   502  			return values, errors.Wrapf(err, "Conversion of UseFieldsOfAddonDescriptor in the config failed")
   503  		}
   504  		m := make(map[string]string)
   505  		for _, uF := range useFields {
   506  			if uF.Use == "" || uF.Rename == "" {
   507  				log.SetErrorCategory(log.ErrorConfiguration)
   508  				return values, errors.Errorf("Invalid UseFieldsOfAddonDescriptor for use %s and renameTo %s", uF.Use, uF.Rename)
   509  			}
   510  			m[uF.Use] = uF.Rename
   511  		}
   512  
   513  		fields := reflect.ValueOf(mR.Repository)
   514  		typeOfS := fields.Type()
   515  		for i := 0; i < fields.NumField(); i++ {
   516  			var value abapbuild.Value
   517  			ValueID := typeOfS.Field(i).Name
   518  			rename, present := m[ValueID]
   519  			if present {
   520  				log.Entry().Infof("Use field %s from addonDescriptor and rename it to %s, the value is %s", ValueID, rename, fields.Field(i).String())
   521  				value.ValueID = rename
   522  				value.Value = fields.Field(i).String()
   523  				values = append(values, value)
   524  			}
   525  		}
   526  		if len(values) != len(useFields) {
   527  			log.SetErrorCategory(log.ErrorConfiguration)
   528  			return values, errors.Errorf("Not all fields in UseFieldsOfAddonDescriptor have been found. Probably a 'use' was used which does not exist")
   529  		}
   530  	}
   531  	return values, nil
   532  }
   533  
   534  func (mR *myRepo) getField(field string) string {
   535  	r := reflect.ValueOf(mR)
   536  	f := reflect.Indirect(r).FieldByName(field)
   537  	return string(f.String())
   538  }
   539  
   540  func (mR *myRepo) amI(field string, operator string, comp string) (bool, error) {
   541  	operators := OperatorCallback{
   542  		"==": Equal,
   543  		"!=": Unequal,
   544  	}
   545  	name := mR.getField(field)
   546  	if fn, ok := operators[operator]; ok {
   547  		return fn(name, comp), nil
   548  	}
   549  	log.SetErrorCategory(log.ErrorConfiguration)
   550  	return false, errors.Errorf("Invalid operator %s", operator)
   551  }
   552  
   553  type OperatorCallback map[string]func(string, string) bool
   554  
   555  func Equal(a, b string) bool {
   556  	return a == b
   557  }
   558  
   559  func Unequal(a, b string) bool {
   560  	return a != b
   561  }