github.com/jaylevin/jenkins-library@v1.230.4/cmd/abapEnvironmentBuild.go (about)

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