get.porter.sh/porter@v1.3.0/pkg/storage/plugin_adapter.go (about) 1 package storage 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "strings" 9 10 "get.porter.sh/porter/pkg/storage/plugins" 11 "go.mongodb.org/mongo-driver/bson" 12 ) 13 14 var _ Store = PluginAdapter{} 15 16 // PluginAdapter converts between the low-level plugin.StorageProtocol which 17 // operates on bson documents, and the document types stored by Porter which are 18 // marshaled using json. 19 // 20 // Specifically it handles converting from bson.Raw to the type specified by 21 // ResultType on plugin.ResultOptions so that you can just cast the result to 22 // the specified type safely. 23 type PluginAdapter struct { 24 plugin plugins.StorageProtocol 25 } 26 27 // NewPluginAdapter wraps the specified storage plugin. 28 func NewPluginAdapter(plugin plugins.StorageProtocol) PluginAdapter { 29 return PluginAdapter{ 30 plugin: plugin, 31 } 32 } 33 34 func (a PluginAdapter) Close() error { 35 if closer, ok := a.plugin.(io.Closer); ok { 36 return closer.Close() 37 } 38 return nil 39 } 40 41 func (a PluginAdapter) Aggregate(ctx context.Context, collection string, opts AggregateOptions, out interface{}) error { 42 rawResults, err := a.plugin.Aggregate(ctx, opts.ToPluginOptions(collection)) 43 if err != nil { 44 return err 45 } 46 47 return a.unmarshalSlice(rawResults, out) 48 } 49 50 func (a PluginAdapter) EnsureIndex(ctx context.Context, opts EnsureIndexOptions) error { 51 return a.plugin.EnsureIndex(ctx, opts.ToPluginOptions()) 52 } 53 54 func (a PluginAdapter) Count(ctx context.Context, collection string, opts CountOptions) (int64, error) { 55 return a.plugin.Count(ctx, opts.ToPluginOptions(collection)) 56 } 57 58 func (a PluginAdapter) Find(ctx context.Context, collection string, opts FindOptions, out interface{}) error { 59 rawResults, err := a.plugin.Find(ctx, opts.ToPluginOptions(collection)) 60 if err != nil { 61 return a.handleError(err, collection) 62 63 } 64 65 return a.unmarshalSlice(rawResults, out) 66 } 67 68 // FindOne queries a collection and returns the first result, returning 69 // ErrNotFound when no results are returned. 70 func (a PluginAdapter) FindOne(ctx context.Context, collection string, opts FindOptions, out interface{}) error { 71 rawResults, err := a.plugin.Find(ctx, opts.ToPluginOptions(collection)) 72 if err != nil { 73 return a.handleError(err, collection) 74 } 75 76 if len(rawResults) == 0 { 77 notFoundErr := ErrNotFound{Collection: collection} 78 if name, ok := opts.Filter["name"]; ok { 79 notFoundErr.Item = fmt.Sprint(name) 80 } 81 return notFoundErr 82 } 83 84 err = a.unmarshal(rawResults[0], out) 85 if err != nil { 86 return fmt.Errorf("could not unmarshal document of type %T: %w", out, err) 87 } 88 89 return nil 90 } 91 92 // unmarshalSlice unpacks a slice of bson documents onto the specified type slice (out) 93 // by going through a temporary representation of the document as json so that we 94 // use the json marshal logic defined on the struct, e.g. if fields have different 95 // names defined with json tags. 96 func (a PluginAdapter) unmarshalSlice(bsonResults []bson.Raw, out interface{}) error { 97 // We want to go from []bson.Raw -> []bson.M -> json -> out (typed struct) 98 99 // Populate a single document with all the results in an intermediate 100 // format of map[string]interface 101 tmpResults := make([]bson.M, len(bsonResults)) 102 for i, bsonResult := range bsonResults { 103 var result bson.M 104 err := bson.Unmarshal(bsonResult, &result) 105 if err != nil { 106 return err 107 } 108 tmpResults[i] = result 109 } 110 111 // Marshal the consolidated document to json 112 data, err := json.Marshal(tmpResults) 113 if err != nil { 114 return fmt.Errorf("error marshaling results into a single result document: %w", err) 115 } 116 117 // Unmarshal the consolidated results onto our destination output 118 err = json.Unmarshal(data, out) 119 if err != nil { 120 return fmt.Errorf("could not unmarshal slice onto type %T: %w", out, err) 121 } 122 123 return nil 124 } 125 126 // unmarshalSlice a bson document onto the specified typed output 127 // by going through a temporary representation of the document as json so that we 128 // use the json marshal logic defined on the struct, e.g. if fields have different 129 // names defined with json tags. 130 func (a PluginAdapter) unmarshal(bsonResult bson.Raw, out interface{}) error { 131 // We want to go from bson.Raw -> bson.M -> json -> out (typed struct) 132 133 var tmpResult bson.M 134 err := bson.Unmarshal(bsonResult, &tmpResult) 135 if err != nil { 136 return err 137 } 138 139 // Marshal the consolidated document to json 140 data, err := json.Marshal(tmpResult) 141 if err != nil { 142 return fmt.Errorf("error marshaling results into a single result document: %w", err) 143 } 144 145 // Unmarshal the consolidated results onto our destination output 146 err = json.Unmarshal(data, out) 147 if err != nil { 148 return fmt.Errorf("could not unmarshal slice onto type %T: %w", out, err) 149 } 150 151 return nil 152 } 153 154 func (a PluginAdapter) Get(ctx context.Context, collection string, opts GetOptions, out interface{}) error { 155 findOpts := opts.ToFindOptions() 156 err := a.FindOne(ctx, collection, findOpts, out) 157 return a.handleError(err, collection) 158 } 159 160 func (a PluginAdapter) Insert(ctx context.Context, collection string, opts InsertOptions) error { 161 pluginOpts, err := opts.ToPluginOptions(collection) 162 if err != nil { 163 return err 164 } 165 166 err = a.plugin.Insert(ctx, pluginOpts) 167 return a.handleError(err, collection) 168 } 169 170 func (a PluginAdapter) Patch(ctx context.Context, collection string, opts PatchOptions) error { 171 err := a.plugin.Patch(ctx, opts.ToPluginOptions(collection)) 172 return a.handleError(err, collection) 173 } 174 175 func (a PluginAdapter) Remove(ctx context.Context, collection string, opts RemoveOptions) error { 176 err := a.plugin.Remove(ctx, opts.ToPluginOptions(collection)) 177 return a.handleError(err, collection) 178 } 179 180 func (a PluginAdapter) Update(ctx context.Context, collection string, opts UpdateOptions) error { 181 pluginOpts, err := opts.ToPluginOptions(collection) 182 if err != nil { 183 return err 184 } 185 err = a.plugin.Update(ctx, pluginOpts) 186 return a.handleError(err, collection) 187 } 188 189 // handleError unwraps errors returned from a plugin (which due to the round trip 190 // through the plugin framework means the original typed error may not be the right type anymore 191 // and turns it back into a well known error such as NotFound. 192 func (a PluginAdapter) handleError(err error, collection string) error { 193 if err != nil && strings.Contains(strings.ToLower(err.Error()), "not found") { 194 return ErrNotFound{Collection: collection} 195 } 196 return err 197 } 198 199 // ErrNotFound indicates that the requested document was not found. 200 // You can test for this error using errors.Is(err, storage.ErrNotFound{}) 201 type ErrNotFound struct { 202 Collection string 203 Item string 204 } 205 206 func (e ErrNotFound) Error() string { 207 var docType string 208 switch e.Collection { 209 case "installations": 210 docType = "Installation" 211 case "runs": 212 docType = "Run" 213 case "results": 214 docType = "Result" 215 case "output": 216 docType = "Output" 217 case "credentials", "parameters": 218 if len(e.Item) > 0 { 219 docType = e.Item 220 } 221 } 222 223 return fmt.Sprintf("%s not found", docType) 224 } 225 226 func (e ErrNotFound) Is(err error) bool { 227 _, ok := err.(ErrNotFound) 228 return ok 229 }