github.com/operator-framework/operator-lifecycle-manager@v0.30.0/pkg/lib/testobj/fixture.go (about)

     1  package testobj
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"reflect"
     7  
     8  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     9  	"k8s.io/apimachinery/pkg/runtime"
    10  	"k8s.io/apimachinery/pkg/util/yaml"
    11  )
    12  
    13  // RuntimeMetaObject is an object with both runtime and metadata level info.
    14  type RuntimeMetaObject interface {
    15  	runtime.Object
    16  	metav1.Object
    17  }
    18  
    19  // FixtureFiller knows how to fill fixtures.
    20  type FixtureFiller interface {
    21  	// NewFixture populates a given fixture.
    22  	Fill(fixture RuntimeMetaObject) RuntimeMetaObject
    23  }
    24  
    25  // FixtureFillerFunc is a function that implements FixtureFiller.
    26  type FixtureFillerFunc func(fixture RuntimeMetaObject) RuntimeMetaObject
    27  
    28  // Fill invokes a FixtureFillerFunc on a fixture.
    29  func (f FixtureFillerFunc) Fill(fixture RuntimeMetaObject) RuntimeMetaObject {
    30  	return f(fixture)
    31  }
    32  
    33  // TypedFixtureFiller is a set of fillers keyed by fixture type.
    34  type TypedFixtureFiller struct {
    35  	fillers map[reflect.Type]FixtureFiller
    36  }
    37  
    38  // Fill populates a fixture if an associated filler has been defined for its type.
    39  // Panics if there's no filler defined for the fixture type.
    40  func (t *TypedFixtureFiller) Fill(fixture RuntimeMetaObject) RuntimeMetaObject {
    41  	if t.fillers == nil {
    42  		t.fillers = map[reflect.Type]FixtureFiller{}
    43  	}
    44  
    45  	// Pick out the correct filler and pass the buck
    46  	ft := reflect.TypeOf(fixture)
    47  	filler := t.fillers[ft]
    48  	if filler == nil {
    49  		panic(fmt.Errorf("unrecognized fixture type: %t", fixture))
    50  	}
    51  
    52  	return filler.Fill(fixture)
    53  }
    54  
    55  // PrototypeFiller is a Filler that copies existing fields from a prototypical instance of a fixture.
    56  type PrototypeFiller struct {
    57  	prototype RuntimeMetaObject
    58  }
    59  
    60  // Fill populates a fixture by copying a prototypical fixture.
    61  // Panics if a given fixture is not the same type as the prototype.
    62  func (p *PrototypeFiller) Fill(fixture RuntimeMetaObject) RuntimeMetaObject {
    63  	// Copy p.proto, fill with fixture meta, and set underlying value
    64  	if reflect.TypeOf(fixture) != reflect.TypeOf(p.prototype) {
    65  		panic(fmt.Errorf("wrong fixture type for filler, have %t want %t", fixture, p.prototype))
    66  	}
    67  
    68  	c := p.prototype.DeepCopyObject()
    69  	vp := reflect.ValueOf(c).Elem()
    70  	reflect.ValueOf(fixture).Elem().Set(vp) // TODO(njhale): do we care about recovering from panics in fixture fillers?
    71  
    72  	return fixture
    73  }
    74  
    75  type fixtureFile struct {
    76  	file    string
    77  	fixture RuntimeMetaObject
    78  }
    79  
    80  // FixtureFillerConfig holds the configuration needed to build a FixtureFiller.
    81  type FixtureFillerConfig struct {
    82  	fixtureFiles      []fixtureFile
    83  	fixturePrototypes []RuntimeMetaObject
    84  }
    85  
    86  func (c *FixtureFillerConfig) apply(options []FixtureFillerOption) {
    87  	for _, option := range options {
    88  		option(c)
    89  	}
    90  }
    91  
    92  // FixtureFillerOption represents a configuration option for building a FixtureFiller.
    93  type FixtureFillerOption func(*FixtureFillerConfig)
    94  
    95  // WithFixtureFile configures a FixtureFiller to use a file to populate fixtures of a given type.
    96  func WithFixtureFile(fixture RuntimeMetaObject, file string) FixtureFillerOption {
    97  	return func(config *FixtureFillerConfig) {
    98  		config.fixtureFiles = append(config.fixtureFiles, fixtureFile{fixture: fixture, file: file})
    99  	}
   100  }
   101  
   102  // WithFixture configures a FixtureFiller to copy the contents of the given fixture to fixtures of the same type.
   103  func WithFixture(fixture RuntimeMetaObject) FixtureFillerOption {
   104  	return func(config *FixtureFillerConfig) {
   105  		config.fixturePrototypes = append(config.fixturePrototypes, fixture)
   106  	}
   107  }
   108  
   109  // NewFixtureFiller builds and returns a new FixtureFiller.
   110  func NewFixtureFiller(options ...FixtureFillerOption) *TypedFixtureFiller {
   111  	config := &FixtureFillerConfig{}
   112  	config.apply(options)
   113  
   114  	// Load files and generate filters by type
   115  	typed := &TypedFixtureFiller{
   116  		fillers: map[reflect.Type]FixtureFiller{},
   117  	}
   118  	for _, fixtureFile := range config.fixtureFiles {
   119  		file := fixtureFile.file
   120  		fixture := fixtureFile.fixture.DeepCopyObject()
   121  		err := func() error {
   122  			// TODO(njhale): DI file decoder
   123  			fileReader, err := os.Open(file)
   124  			if err != nil {
   125  				return fmt.Errorf("unable to read file %s: %s", file, err)
   126  			}
   127  			defer fileReader.Close()
   128  
   129  			decoder := yaml.NewYAMLOrJSONDecoder(fileReader, 30)
   130  
   131  			return decoder.Decode(fixture)
   132  		}()
   133  
   134  		if err != nil {
   135  			panic(err)
   136  		}
   137  
   138  		typed.fillers[reflect.TypeOf(fixture)] = &PrototypeFiller{prototype: fixture.(RuntimeMetaObject)}
   139  	}
   140  
   141  	// Load in-memory fixtures
   142  	for _, prototype := range config.fixturePrototypes {
   143  		if prototype == nil {
   144  			panic("nil fixtures not allowed")
   145  		}
   146  
   147  		typed.fillers[reflect.TypeOf(prototype)] = &PrototypeFiller{prototype: prototype}
   148  	}
   149  
   150  	return typed
   151  }