...

Source file src/sigs.k8s.io/structured-merge-diff/v4/internal/fixture/state.go

Documentation: sigs.k8s.io/structured-merge-diff/v4/internal/fixture

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package fixture
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  
    25  	"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
    26  	"sigs.k8s.io/structured-merge-diff/v4/merge"
    27  	"sigs.k8s.io/structured-merge-diff/v4/typed"
    28  )
    29  
    30  // For the sake of tests, a parser is something that can retrieve a
    31  // ParseableType.
    32  type Parser interface {
    33  	Type(string) typed.ParseableType
    34  }
    35  
    36  // SameVersionParser can be used if all the versions are actually using the same type.
    37  type SameVersionParser struct {
    38  	T typed.ParseableType
    39  }
    40  
    41  func (p SameVersionParser) Type(_ string) typed.ParseableType {
    42  	return p.T
    43  }
    44  
    45  // DeducedParser is a parser that is deduced no matter what the version
    46  // specified.
    47  var DeducedParser = SameVersionParser{
    48  	T: typed.DeducedParseableType,
    49  }
    50  
    51  // State of the current test in terms of live object. One can check at
    52  // any time that Live and Managers match the expectations.
    53  //
    54  // The parser will look for the type by using the APIVersion of the
    55  // object it's trying to parse. If trying to parse a "v1" object, a
    56  // corresponding "v1" type should exist in the schema. If all the
    57  // versions should map to the same type, or to a DeducedParseableType,
    58  // one can use the SameVersionParser or the DeducedParser types defined
    59  // in this package.
    60  type State struct {
    61  	Live     *typed.TypedValue
    62  	Parser   Parser
    63  	Managers fieldpath.ManagedFields
    64  	Updater  *merge.Updater
    65  }
    66  
    67  // FixTabsOrDie counts the number of tab characters preceding the first
    68  // line in the given yaml object. It removes that many tabs from every
    69  // line. It panics (it's a test funtion) if some line has fewer tabs
    70  // than the first line.
    71  //
    72  // The purpose of this is to make it easier to read tests.
    73  func FixTabsOrDie(in typed.YAMLObject) typed.YAMLObject {
    74  	lines := bytes.Split([]byte(in), []byte{'\n'})
    75  	if len(lines[0]) == 0 && len(lines) > 1 {
    76  		lines = lines[1:]
    77  	}
    78  	// Create prefix made of tabs that we want to remove.
    79  	var prefix []byte
    80  	for _, c := range lines[0] {
    81  		if c != '\t' {
    82  			break
    83  		}
    84  		prefix = append(prefix, byte('\t'))
    85  	}
    86  	// Remove prefix from all tabs, fail otherwise.
    87  	for i := range lines {
    88  		line := lines[i]
    89  		// It's OK for the last line to be blank (trailing \n)
    90  		if i == len(lines)-1 && len(line) <= len(prefix) && bytes.TrimSpace(line) == nil {
    91  			lines[i] = []byte{}
    92  			break
    93  		}
    94  		if !bytes.HasPrefix(line, prefix) {
    95  			panic(fmt.Errorf("line %d doesn't start with expected number (%d) of tabs: %v", i, len(prefix), string(line)))
    96  		}
    97  		lines[i] = line[len(prefix):]
    98  	}
    99  	return typed.YAMLObject(bytes.Join(lines, []byte{'\n'}))
   100  }
   101  
   102  func (s *State) checkInit(version fieldpath.APIVersion) error {
   103  	if s.Live == nil {
   104  		obj, err := s.Parser.Type(string(version)).FromUnstructured(nil)
   105  		if err != nil {
   106  			return fmt.Errorf("failed to create new empty object: %v", err)
   107  		}
   108  		s.Live = obj
   109  	}
   110  	return nil
   111  }
   112  
   113  func (s *State) UpdateObject(tv *typed.TypedValue, version fieldpath.APIVersion, manager string) error {
   114  	err := s.checkInit(version)
   115  	if err != nil {
   116  		return err
   117  	}
   118  	s.Live, err = s.Updater.Converter.Convert(s.Live, version)
   119  	if err != nil {
   120  		return err
   121  	}
   122  	newObj, managers, err := s.Updater.Update(s.Live, tv, version, s.Managers, manager)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	s.Live = newObj
   127  	s.Managers = managers
   128  
   129  	return nil
   130  }
   131  
   132  // Update the current state with the passed in object
   133  func (s *State) Update(obj typed.YAMLObject, version fieldpath.APIVersion, manager string) error {
   134  	tv, err := s.Parser.Type(string(version)).FromYAML(FixTabsOrDie(obj), typed.AllowDuplicates)
   135  	if err != nil {
   136  		return err
   137  	}
   138  	return s.UpdateObject(tv, version, manager)
   139  }
   140  
   141  func (s *State) ApplyObject(tv *typed.TypedValue, version fieldpath.APIVersion, manager string, force bool) error {
   142  	err := s.checkInit(version)
   143  	if err != nil {
   144  		return err
   145  	}
   146  	s.Live, err = s.Updater.Converter.Convert(s.Live, version)
   147  	if err != nil {
   148  		return err
   149  	}
   150  	new, managers, err := s.Updater.Apply(s.Live, tv, version, s.Managers, manager, force)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	s.Managers = managers
   155  	if new != nil {
   156  		s.Live = new
   157  	}
   158  	return nil
   159  }
   160  
   161  // Apply the passed in object to the current state
   162  func (s *State) Apply(obj typed.YAMLObject, version fieldpath.APIVersion, manager string, force bool) error {
   163  	tv, err := s.Parser.Type(string(version)).FromYAML(FixTabsOrDie(obj))
   164  	if err != nil {
   165  		return err
   166  	}
   167  	return s.ApplyObject(tv, version, manager, force)
   168  }
   169  
   170  // CompareLive takes a YAML string and returns the comparison with the
   171  // current live object or an error.
   172  func (s *State) CompareLive(obj typed.YAMLObject, version fieldpath.APIVersion) (string, error) {
   173  	obj = FixTabsOrDie(obj)
   174  	if err := s.checkInit(version); err != nil {
   175  		return "", err
   176  	}
   177  	tv, err := s.Parser.Type(string(version)).FromYAML(obj, typed.AllowDuplicates)
   178  	if err != nil {
   179  		return "", err
   180  	}
   181  	live, err := s.Updater.Converter.Convert(s.Live, version)
   182  	if err != nil {
   183  		return "", err
   184  	}
   185  	tvu := convertMapAnyToMapString(tv.AsValue().Unstructured())
   186  	liveu := convertMapAnyToMapString(live.AsValue().Unstructured())
   187  	return cmp.Diff(tvu, liveu), nil
   188  }
   189  
   190  func convertMapAnyToMapString(obj interface{}) interface{} {
   191  	switch m := obj.(type) {
   192  	case map[string]interface{}:
   193  		out := map[string]interface{}{}
   194  		for key, value := range m {
   195  			out[key] = convertMapAnyToMapString(value)
   196  		}
   197  		return out
   198  	case map[interface{}]interface{}:
   199  		out := map[string]interface{}{}
   200  		for key, value := range m {
   201  			out[key.(string)] = convertMapAnyToMapString(value)
   202  		}
   203  		return out
   204  	case []interface{}:
   205  		out := []interface{}{}
   206  		for _, value := range m {
   207  			out = append(out, convertMapAnyToMapString(value))
   208  		}
   209  		return out
   210  	default:
   211  		return obj
   212  	}
   213  }
   214  
   215  // dummyConverter doesn't convert, it just returns the same exact object, as long as a version is provided.
   216  type dummyConverter struct{}
   217  
   218  var _ merge.Converter = dummyConverter{}
   219  
   220  // Convert returns the object given in input, not doing any conversion.
   221  func (dummyConverter) Convert(v *typed.TypedValue, version fieldpath.APIVersion) (*typed.TypedValue, error) {
   222  	if len(version) == 0 {
   223  		return nil, fmt.Errorf("cannot convert to invalid version: %q", version)
   224  	}
   225  	return v, nil
   226  }
   227  
   228  func (dummyConverter) IsMissingVersionError(err error) bool {
   229  	return false
   230  }
   231  
   232  // Operation is a step that will run when building a table-driven test.
   233  type Operation interface {
   234  	run(*State) error
   235  	preprocess(Parser) (Operation, error)
   236  }
   237  
   238  func hasConflict(conflicts merge.Conflicts, conflict merge.Conflict) bool {
   239  	for i := range conflicts {
   240  		if conflict.Equals(conflicts[i]) {
   241  			return true
   242  		}
   243  	}
   244  	return false
   245  }
   246  
   247  func addedConflicts(one, other merge.Conflicts) merge.Conflicts {
   248  	added := merge.Conflicts{}
   249  	for _, conflict := range other {
   250  		if !hasConflict(one, conflict) {
   251  			added = append(added, conflict)
   252  		}
   253  	}
   254  	return added
   255  }
   256  
   257  // Apply is a type of operation. It is a non-forced apply run by a
   258  // manager with a given object. Since non-forced apply operation can
   259  // conflict, the user can specify the expected conflicts. If conflicts
   260  // don't match, an error will occur.
   261  type Apply struct {
   262  	Manager    string
   263  	APIVersion fieldpath.APIVersion
   264  	Object     typed.YAMLObject
   265  	Conflicts  merge.Conflicts
   266  }
   267  
   268  var _ Operation = &Apply{}
   269  
   270  func (a Apply) run(state *State) error {
   271  	p, err := a.preprocess(state.Parser)
   272  	if err != nil {
   273  		return err
   274  	}
   275  	return p.run(state)
   276  }
   277  
   278  func (a Apply) preprocess(parser Parser) (Operation, error) {
   279  	tv, err := parser.Type(string(a.APIVersion)).FromYAML(FixTabsOrDie(a.Object))
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  	return ApplyObject{
   284  		Manager:    a.Manager,
   285  		APIVersion: a.APIVersion,
   286  		Object:     tv,
   287  		Conflicts:  a.Conflicts,
   288  	}, nil
   289  }
   290  
   291  type ApplyObject struct {
   292  	Manager    string
   293  	APIVersion fieldpath.APIVersion
   294  	Object     *typed.TypedValue
   295  	Conflicts  merge.Conflicts
   296  }
   297  
   298  var _ Operation = &ApplyObject{}
   299  
   300  func (a ApplyObject) run(state *State) error {
   301  	err := state.ApplyObject(a.Object, a.APIVersion, a.Manager, false)
   302  	if err != nil {
   303  		if _, ok := err.(merge.Conflicts); !ok || a.Conflicts == nil {
   304  			return err
   305  		}
   306  	}
   307  	if a.Conflicts != nil {
   308  		conflicts := merge.Conflicts{}
   309  		if err != nil {
   310  			conflicts = err.(merge.Conflicts)
   311  		}
   312  		if len(addedConflicts(a.Conflicts, conflicts)) != 0 || len(addedConflicts(conflicts, a.Conflicts)) != 0 {
   313  			return fmt.Errorf("Expected conflicts:\n%v\ngot\n%v\nadded:\n%v\nremoved:\n%v",
   314  				a.Conflicts.Error(),
   315  				conflicts.Error(),
   316  				addedConflicts(a.Conflicts, conflicts).Error(),
   317  				addedConflicts(conflicts, a.Conflicts).Error(),
   318  			)
   319  		}
   320  	}
   321  	return nil
   322  }
   323  
   324  func (a ApplyObject) preprocess(parser Parser) (Operation, error) {
   325  	return a, nil
   326  }
   327  
   328  // ForceApply is a type of operation. It is a forced-apply run by a
   329  // manager with a given object. Any error will be returned.
   330  type ForceApply struct {
   331  	Manager    string
   332  	APIVersion fieldpath.APIVersion
   333  	Object     typed.YAMLObject
   334  }
   335  
   336  var _ Operation = &ForceApply{}
   337  
   338  func (f ForceApply) run(state *State) error {
   339  	return state.Apply(f.Object, f.APIVersion, f.Manager, true)
   340  }
   341  
   342  func (f ForceApply) preprocess(parser Parser) (Operation, error) {
   343  	tv, err := parser.Type(string(f.APIVersion)).FromYAML(FixTabsOrDie(f.Object))
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  	return ForceApplyObject{
   348  		Manager:    f.Manager,
   349  		APIVersion: f.APIVersion,
   350  		Object:     tv,
   351  	}, nil
   352  }
   353  
   354  // ForceApplyObject is a type of operation. It is a forced-apply run by
   355  // a manager with a given object. Any error will be returned.
   356  type ForceApplyObject struct {
   357  	Manager    string
   358  	APIVersion fieldpath.APIVersion
   359  	Object     *typed.TypedValue
   360  }
   361  
   362  var _ Operation = &ForceApplyObject{}
   363  
   364  func (f ForceApplyObject) run(state *State) error {
   365  	return state.ApplyObject(f.Object, f.APIVersion, f.Manager, true)
   366  }
   367  
   368  func (f ForceApplyObject) preprocess(parser Parser) (Operation, error) {
   369  	return f, nil
   370  }
   371  
   372  // ExtractApply is a type of operation. It simulates extracting an object
   373  // the state based on the manager you have applied with, merging the
   374  // apply object with that extracted object and reapplying that.
   375  type ExtractApply struct {
   376  	Manager    string
   377  	APIVersion fieldpath.APIVersion
   378  	Object     typed.YAMLObject
   379  }
   380  
   381  var _ Operation = &ExtractApply{}
   382  
   383  func (e ExtractApply) run(state *State) error {
   384  	p, err := e.preprocess(state.Parser)
   385  	if err != nil {
   386  		return err
   387  	}
   388  	return p.run(state)
   389  }
   390  
   391  func (e ExtractApply) preprocess(parser Parser) (Operation, error) {
   392  
   393  	tv, err := parser.Type(string(e.APIVersion)).FromYAML(FixTabsOrDie(e.Object))
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  	return ExtractApplyObject{
   398  		Manager:    e.Manager,
   399  		APIVersion: e.APIVersion,
   400  		Object:     tv,
   401  	}, nil
   402  }
   403  
   404  type ExtractApplyObject struct {
   405  	Manager    string
   406  	APIVersion fieldpath.APIVersion
   407  	Object     *typed.TypedValue
   408  }
   409  
   410  var _ Operation = &ExtractApplyObject{}
   411  
   412  func (e ExtractApplyObject) run(state *State) error {
   413  	if state.Live == nil {
   414  		return state.ApplyObject(e.Object, e.APIVersion, e.Manager, true)
   415  	}
   416  	// Get object from state and convert it to current APIVersion
   417  	current, err := state.Updater.Converter.Convert(state.Live, e.APIVersion)
   418  	if err != nil {
   419  		return err
   420  	}
   421  	// Get set based on the manager you've applied with
   422  	set := fieldpath.NewSet()
   423  	mgr := state.Managers[e.Manager]
   424  	if mgr != nil {
   425  		// we cannot extract a set that is for a different version
   426  		if mgr.APIVersion() != e.APIVersion {
   427  			return fmt.Errorf("existing managed fieldpath set APIVersion (%s) differs from desired (%s), unable to extract", mgr.APIVersion(), e.APIVersion)
   428  		}
   429  		// trying to extract the fieldSet directly will return everything
   430  		// under the first path in the set, so we must filter out all
   431  		// the non-leaf nodes from the fieldSet
   432  		set = mgr.Set().Leaves()
   433  	}
   434  	// ExtractFields from the state object based on the set
   435  	extracted := current.ExtractItems(set)
   436  	// Merge ApplyObject on top of the extracted object
   437  	obj, err := extracted.Merge(e.Object)
   438  	if err != nil {
   439  		return err
   440  	}
   441  	// Reapply that to the state
   442  	return state.ApplyObject(obj, e.APIVersion, e.Manager, true)
   443  }
   444  
   445  func (e ExtractApplyObject) preprocess(parser Parser) (Operation, error) {
   446  	return e, nil
   447  }
   448  
   449  // Update is a type of operation. It is a controller type of
   450  // update. Errors are passed along.
   451  type Update struct {
   452  	Manager    string
   453  	APIVersion fieldpath.APIVersion
   454  	Object     typed.YAMLObject
   455  }
   456  
   457  var _ Operation = &Update{}
   458  
   459  func (u Update) run(state *State) error {
   460  	return state.Update(u.Object, u.APIVersion, u.Manager)
   461  }
   462  
   463  func (u Update) preprocess(parser Parser) (Operation, error) {
   464  	tv, err := parser.Type(string(u.APIVersion)).FromYAML(FixTabsOrDie(u.Object), typed.AllowDuplicates)
   465  	if err != nil {
   466  		return nil, err
   467  	}
   468  	return UpdateObject{
   469  		Manager:    u.Manager,
   470  		APIVersion: u.APIVersion,
   471  		Object:     tv,
   472  	}, nil
   473  }
   474  
   475  // UpdateObject is a type of operation. It is a controller type of
   476  // update. Errors are passed along.
   477  type UpdateObject struct {
   478  	Manager    string
   479  	APIVersion fieldpath.APIVersion
   480  	Object     *typed.TypedValue
   481  }
   482  
   483  var _ Operation = &Update{}
   484  
   485  func (u UpdateObject) run(state *State) error {
   486  	return state.UpdateObject(u.Object, u.APIVersion, u.Manager)
   487  }
   488  
   489  func (f UpdateObject) preprocess(parser Parser) (Operation, error) {
   490  	return f, nil
   491  }
   492  
   493  // ChangeParser is a type of operation. It simulates making changes a schema without versioning
   494  // the schema. This can be used to test the behavior of making backward compatible schema changes,
   495  // e.g. setting "elementRelationship: atomic" on an existing struct. It also may be used to ensure
   496  // that backward incompatible changes are detected appropriately.
   497  type ChangeParser struct {
   498  	Parser *typed.Parser
   499  }
   500  
   501  var _ Operation = &ChangeParser{}
   502  
   503  func (cs ChangeParser) run(state *State) error {
   504  	state.Parser = cs.Parser
   505  	// Swap the schema in for use with the live object so it merges.
   506  	// If the schema is incompatible, this will fail validation.
   507  
   508  	liveWithNewSchema, err := typed.AsTyped(state.Live.AsValue(), &cs.Parser.Schema, state.Live.TypeRef())
   509  	if err != nil {
   510  		return err
   511  	}
   512  	state.Live = liveWithNewSchema
   513  	return nil
   514  }
   515  
   516  func (cs ChangeParser) preprocess(_ Parser) (Operation, error) {
   517  	return cs, nil
   518  }
   519  
   520  // TestCase is the list of operations that need to be run, as well as
   521  // the object/managedfields as they are supposed to look like after all
   522  // the operations have been successfully performed. If Object/Managed is
   523  // not specified, then the comparison is not performed (any object or
   524  // managed field will pass). Any error (conflicts aside) happen while
   525  // running the operation, that error will be returned right away.
   526  type TestCase struct {
   527  	// Ops is the list of operations to run sequentially
   528  	Ops []Operation
   529  	// Object, if not empty, is the object as it's expected to
   530  	// be after all the operations are run.
   531  	Object typed.YAMLObject
   532  	// APIVersion should be set if the object is non-empty and
   533  	// describes the version of the object to compare to.
   534  	APIVersion fieldpath.APIVersion
   535  	// Managed, if not nil, is the ManagedFields as expected
   536  	// after all operations are run.
   537  	Managed fieldpath.ManagedFields
   538  	// ReportInputOnNoop if we don't want to compare the output and
   539  	// always return it.
   540  	ReturnInputOnNoop bool
   541  	// IgnoredFields containing the set to ignore for every version
   542  	IgnoredFields map[fieldpath.APIVersion]*fieldpath.Set
   543  }
   544  
   545  // Test runs the test-case using the given parser and a dummy converter.
   546  func (tc TestCase) Test(parser Parser) error {
   547  	return tc.TestWithConverter(parser, &dummyConverter{})
   548  }
   549  
   550  // Bench runs the test-case using the given parser and a dummy converter, but
   551  // doesn't check exit conditions--see the comment for BenchWithConverter.
   552  func (tc TestCase) Bench(parser Parser) error {
   553  	return tc.BenchWithConverter(parser, &dummyConverter{})
   554  }
   555  
   556  // Preprocess all the operations by parsing the yaml before-hand.
   557  func (tc TestCase) PreprocessOperations(parser Parser) error {
   558  	for i := range tc.Ops {
   559  		op, err := tc.Ops[i].preprocess(parser)
   560  		if err != nil {
   561  			return err
   562  		}
   563  		tc.Ops[i] = op
   564  	}
   565  	return nil
   566  }
   567  
   568  // BenchWithConverter runs the test-case using the given parser and converter,
   569  // but doesn't do any comparison operations aftewards; you should probably run
   570  // TestWithConverter once and reset the benchmark, to make sure the test case
   571  // actually passes..
   572  func (tc TestCase) BenchWithConverter(parser Parser, converter merge.Converter) error {
   573  	updaterBuilder := merge.UpdaterBuilder{
   574  		Converter:         converter,
   575  		IgnoredFields:     tc.IgnoredFields,
   576  		ReturnInputOnNoop: tc.ReturnInputOnNoop,
   577  	}
   578  	state := State{
   579  		Updater: updaterBuilder.BuildUpdater(),
   580  		Parser:  parser,
   581  	}
   582  	// We currently don't have any test that converts, we can take
   583  	// care of that later.
   584  	for i, ops := range tc.Ops {
   585  		err := ops.run(&state)
   586  		if err != nil {
   587  			return fmt.Errorf("failed operation %d: %v", i, err)
   588  		}
   589  	}
   590  	return nil
   591  }
   592  
   593  // TestWithConverter runs the test-case using the given parser and converter.
   594  func (tc TestCase) TestWithConverter(parser Parser, converter merge.Converter) error {
   595  	updaterBuilder := merge.UpdaterBuilder{
   596  		Converter:         converter,
   597  		IgnoredFields:     tc.IgnoredFields,
   598  		ReturnInputOnNoop: tc.ReturnInputOnNoop,
   599  	}
   600  	state := State{
   601  		Updater: updaterBuilder.BuildUpdater(),
   602  		Parser:  parser,
   603  	}
   604  	for i, ops := range tc.Ops {
   605  		err := ops.run(&state)
   606  		if err != nil {
   607  			return fmt.Errorf("failed operation %d: %v", i, err)
   608  		}
   609  	}
   610  
   611  	// If LastObject was specified, compare it with LiveState
   612  	if tc.Object != typed.YAMLObject("") {
   613  		comparison, err := state.CompareLive(tc.Object, tc.APIVersion)
   614  		if err != nil {
   615  			return fmt.Errorf("failed to compare live with config: %v", err)
   616  		}
   617  		if comparison != "" {
   618  			return fmt.Errorf("expected live and config to be the same:\n%v\n", comparison)
   619  		}
   620  	}
   621  
   622  	if tc.Managed != nil {
   623  		if diff := state.Managers.Difference(tc.Managed); len(diff) != 0 {
   624  			return fmt.Errorf("expected Managers to be:\n%v\ngot:\n%v\ndiff:\n%v", tc.Managed, state.Managers, diff)
   625  		}
   626  	}
   627  
   628  	// Fail if any empty sets are present in the managers
   629  	for manager, set := range state.Managers {
   630  		if set.Set().Empty() {
   631  			return fmt.Errorf("expected Managers to have no empty sets, but found one managed by %v", manager)
   632  		}
   633  	}
   634  
   635  	return nil
   636  }
   637  

View as plain text