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 }