get.porter.sh/porter@v1.3.0/pkg/storage/installation_store.go (about) 1 package storage 2 3 import ( 4 "context" 5 "errors" 6 7 "get.porter.sh/porter/pkg/tracing" 8 "go.mongodb.org/mongo-driver/bson" 9 ) 10 11 const ( 12 CollectionInstallations = "installations" 13 CollectionRuns = "runs" 14 CollectionResults = "results" 15 CollectionOutputs = "outputs" 16 ) 17 18 var _ InstallationProvider = InstallationStore{} 19 20 // InstallationStore is a persistent store for installation documents. 21 type InstallationStore struct { 22 store Store 23 encrypt EncryptionHandler 24 decrypt EncryptionHandler 25 } 26 27 // NewInstallationStore creates a persistent store for installations using the specified 28 // backing datastore. 29 func NewInstallationStore(datastore Store) InstallationStore { 30 return InstallationStore{ 31 store: datastore, 32 encrypt: noOpEncryptionHandler, 33 decrypt: noOpEncryptionHandler, 34 } 35 } 36 37 // EnsureInstallationIndices created indices on the installations collection. 38 func EnsureInstallationIndices(ctx context.Context, store Store) error { 39 ctx, span := tracing.StartSpan(ctx) 40 defer span.EndSpan() 41 42 span.Debug("Initializing installation collection indices") 43 44 opts := EnsureIndexOptions{ 45 Indices: []Index{ 46 // query installations by a namespace (list) or namespace + name (get) 47 {Collection: CollectionInstallations, Keys: []string{"namespace", "name"}, Unique: true}, 48 // query runs by installation (list) 49 {Collection: CollectionRuns, Keys: []string{"namespace", "installation"}}, 50 // query results by installation (delete or batch get) 51 {Collection: CollectionResults, Keys: []string{"namespace", "installation"}}, 52 // query results by run (list) 53 {Collection: CollectionResults, Keys: []string{"runId"}}, 54 // query most recent outputs by run (porter installation run show, when we list outputs) 55 {Collection: CollectionOutputs, Keys: []string{"namespace", "installation", "-resultId"}}, 56 // query outputs by result (list) 57 {Collection: CollectionOutputs, Keys: []string{"resultId", "name"}, Unique: true}, 58 // query most recent outputs by name for an installation 59 {Collection: CollectionOutputs, Keys: []string{"namespace", "installation", "name", "-resultId"}}, 60 }, 61 } 62 63 err := store.EnsureIndex(ctx, opts) 64 return span.Error(err) 65 } 66 67 func (s InstallationStore) ListInstallations(ctx context.Context, listOptions ListOptions) ([]Installation, error) { 68 _, log := tracing.StartSpan(ctx) 69 defer log.EndSpan() 70 71 var out []Installation 72 err := s.store.Find(ctx, CollectionInstallations, listOptions.ToFindOptions(), &out) 73 return out, err 74 } 75 76 func (s InstallationStore) ListRuns(ctx context.Context, namespace string, installation string) ([]Run, map[string][]Result, error) { 77 var runs []Run 78 var err error 79 var results []Result 80 81 opts := FindOptions{ 82 Sort: []string{"_id"}, 83 Filter: bson.M{ 84 "namespace": namespace, 85 "installation": installation, 86 }, 87 } 88 err = s.store.Find(ctx, CollectionRuns, opts, &runs) 89 if err != nil { 90 return nil, nil, err 91 } 92 93 err = s.store.Find(ctx, CollectionResults, opts, &results) 94 if err != nil { 95 return runs, nil, err 96 } 97 98 resultsMap := make(map[string][]Result, len(runs)) 99 100 for _, run := range runs { 101 resultsMap[run.ID] = []Result{} 102 } 103 104 for _, res := range results { 105 if _, ok := resultsMap[res.RunID]; ok { 106 resultsMap[res.RunID] = append(resultsMap[res.RunID], res) 107 } 108 } 109 110 return runs, resultsMap, err 111 } 112 113 func (s InstallationStore) ListResults(ctx context.Context, runID string) ([]Result, error) { 114 var out []Result 115 opts := FindOptions{ 116 Sort: []string{"_id"}, 117 Filter: bson.M{ 118 "runId": runID, 119 }, 120 } 121 err := s.store.Find(ctx, CollectionResults, opts, &out) 122 return out, err 123 } 124 125 func (s InstallationStore) ListOutputs(ctx context.Context, resultID string) ([]Output, error) { 126 var out []Output 127 opts := FindOptions{ 128 Sort: []string{"resultId", "name"}, 129 Filter: bson.M{ 130 "resultId": resultID, 131 }, 132 } 133 err := s.store.Find(ctx, CollectionOutputs, opts, &out) 134 return out, err 135 } 136 137 func (s InstallationStore) FindInstallations(ctx context.Context, findOpts FindOptions) ([]Installation, error) { 138 _, log := tracing.StartSpan(ctx) 139 defer log.EndSpan() 140 141 var out []Installation 142 err := s.store.Find(ctx, CollectionInstallations, findOpts, &out) 143 return out, err 144 } 145 146 func (s InstallationStore) GetInstallation(ctx context.Context, namespace string, name string) (Installation, error) { 147 var out Installation 148 149 opts := FindOptions{ 150 Filter: bson.M{ 151 "namespace": namespace, 152 "name": name, 153 }, 154 } 155 156 err := s.store.FindOne(ctx, CollectionInstallations, opts, &out) 157 158 return out, err 159 } 160 161 func (s InstallationStore) GetRun(ctx context.Context, id string) (Run, error) { 162 var out Run 163 opts := GetOptions{ID: id} 164 err := s.store.Get(ctx, CollectionRuns, opts, &out) 165 return out, err 166 } 167 168 func (s InstallationStore) GetResult(ctx context.Context, id string) (Result, error) { 169 var out Result 170 opts := GetOptions{ID: id} 171 err := s.store.Get(ctx, CollectionResults, opts, &out) 172 return out, err 173 } 174 175 func (s InstallationStore) GetLastRun(ctx context.Context, namespace string, installation string) (Run, error) { 176 var out []Run 177 opts := FindOptions{ 178 Sort: []string{"-_id"}, 179 Limit: 1, 180 Filter: bson.M{ 181 "namespace": namespace, 182 "installation": installation, 183 }, 184 } 185 err := s.store.Find(ctx, CollectionRuns, opts, &out) 186 if err != nil { 187 return Run{}, err 188 } 189 if len(out) == 0 { 190 return Run{}, ErrNotFound{Collection: CollectionRuns} 191 } 192 return out[0], err 193 } 194 195 func (s InstallationStore) GetLastOutput(ctx context.Context, namespace string, installation string, name string) (Output, error) { 196 var out Output 197 opts := FindOptions{ 198 Sort: []string{"-_id"}, 199 Limit: 1, 200 Filter: bson.M{ 201 "namespace": namespace, 202 "installation": installation, 203 "name": name, 204 }, 205 } 206 err := s.store.FindOne(ctx, CollectionOutputs, opts, &out) 207 return out, err 208 } 209 210 func (s InstallationStore) GetLastOutputs(ctx context.Context, namespace string, installation string) (Outputs, error) { 211 var groupedOutputs []struct { 212 ID string `json:"_id"` 213 LastOutput Output `json:"lastOutput"` 214 } 215 opts := AggregateOptions{ 216 Pipeline: []bson.D{ 217 // List outputs by installation 218 {{Key: "$match", Value: bson.M{ 219 "namespace": namespace, 220 "installation": installation, 221 }}}, 222 // Reverse sort them (newest on top) 223 {{Key: "$sort", Value: bson.D{ 224 {Key: "namespace", Value: 1}, 225 {Key: "installation", Value: 1}, 226 {Key: "name", Value: 1}, 227 {Key: "resultId", Value: -1}, 228 }}}, 229 // Group them by output name and select the last value for each output 230 {{Key: "$group", Value: bson.D{ 231 {Key: "_id", Value: "$name"}, 232 {Key: "lastOutput", Value: bson.M{"$first": "$$ROOT"}}, 233 }}}, 234 }, 235 } 236 err := s.store.Aggregate(ctx, CollectionOutputs, opts, &groupedOutputs) 237 238 lastOutputs := make([]Output, len(groupedOutputs)) 239 for i, groupedOutput := range groupedOutputs { 240 lastOutputs[i] = groupedOutput.LastOutput 241 } 242 243 return NewOutputs(lastOutputs), err 244 } 245 246 func (s InstallationStore) GetLogs(ctx context.Context, runID string) (string, bool, error) { 247 var out Output 248 opts := FindOptions{ 249 Sort: []string{"resultId"}, // get logs from the last result for a run 250 Filter: bson.M{ 251 "runId": runID, 252 "name": "io.cnab.outputs.invocationImageLogs", 253 }, 254 Limit: 1, 255 } 256 err := s.store.FindOne(ctx, CollectionOutputs, opts, &out) 257 if errors.Is(err, ErrNotFound{}) { 258 return "", false, nil 259 } 260 return string(out.Value), err == nil, err 261 } 262 263 func (s InstallationStore) GetLastLogs(ctx context.Context, namespace string, installation string) (string, bool, error) { 264 var out Output 265 opts := FindOptions{ 266 Sort: []string{"-resultId"}, // get logs from the last result for a run 267 Filter: bson.M{ 268 "namespace": namespace, 269 "installation": installation, 270 "name": "io.cnab.outputs.invocationImageLogs", 271 }, 272 Limit: 1, 273 } 274 err := s.store.FindOne(ctx, CollectionOutputs, opts, &out) 275 if errors.Is(err, ErrNotFound{}) { 276 return "", false, nil 277 } 278 return string(out.Value), err == nil, err 279 } 280 281 func (s InstallationStore) InsertInstallation(ctx context.Context, installation Installation) error { 282 installation.SchemaVersion = DefaultInstallationSchemaVersion 283 opts := InsertOptions{ 284 Documents: []interface{}{installation}, 285 } 286 return s.store.Insert(ctx, CollectionInstallations, opts) 287 } 288 289 func (s InstallationStore) InsertRun(ctx context.Context, run Run) error { 290 opts := InsertOptions{ 291 Documents: []interface{}{run}, 292 } 293 return s.store.Insert(ctx, CollectionRuns, opts) 294 } 295 296 func (s InstallationStore) InsertResult(ctx context.Context, result Result) error { 297 opts := InsertOptions{ 298 Documents: []interface{}{result}, 299 } 300 return s.store.Insert(ctx, CollectionResults, opts) 301 } 302 303 func (s InstallationStore) InsertOutput(ctx context.Context, output Output) error { 304 opts := InsertOptions{ 305 Documents: []interface{}{output}, 306 } 307 return s.store.Insert(ctx, CollectionOutputs, opts) 308 } 309 310 func (s InstallationStore) UpdateInstallation(ctx context.Context, installation Installation) error { 311 installation.SchemaVersion = DefaultInstallationSchemaVersion 312 opts := UpdateOptions{ 313 Document: installation, 314 } 315 return s.store.Update(ctx, CollectionInstallations, opts) 316 } 317 318 func (s InstallationStore) UpsertRun(ctx context.Context, run Run) error { 319 opts := UpdateOptions{ 320 Upsert: true, 321 Document: run, 322 } 323 return s.store.Update(ctx, CollectionRuns, opts) 324 } 325 326 func (s InstallationStore) UpsertInstallation(ctx context.Context, installation Installation) error { 327 installation.SchemaVersion = DefaultInstallationSchemaVersion 328 opts := UpdateOptions{ 329 Upsert: true, 330 Document: installation, 331 } 332 return s.store.Update(ctx, CollectionInstallations, opts) 333 } 334 335 // RemoveInstallation and all associated data. 336 func (s InstallationStore) RemoveInstallation(ctx context.Context, namespace string, name string) error { 337 removeInstallation := RemoveOptions{ 338 Filter: bson.M{ 339 "namespace": namespace, 340 "name": name, 341 }, 342 } 343 err := s.store.Remove(ctx, CollectionInstallations, removeInstallation) 344 if err != nil { 345 return err 346 } 347 348 // Find associated documents 349 removeChildDocs := RemoveOptions{ 350 Filter: bson.M{ 351 "namespace": namespace, 352 "installation": name, 353 }, 354 All: true, 355 } 356 357 // Delete runs 358 err = s.store.Remove(ctx, CollectionRuns, removeChildDocs) 359 if err != nil { 360 return err 361 } 362 363 // Delete results 364 err = s.store.Remove(ctx, CollectionResults, removeChildDocs) 365 if err != nil { 366 return err 367 } 368 369 // Delete outputs 370 err = s.store.Remove(ctx, CollectionOutputs, removeChildDocs) 371 if err != nil { 372 return err 373 } 374 375 return nil 376 } 377 378 // EncryptionHandler is a function that transforms data by encrypting or decrypting it. 379 type EncryptionHandler func([]byte) ([]byte, error) 380 381 // noOpEncryptHandler is used when no handler is specified. 382 var noOpEncryptionHandler = func(data []byte) ([]byte, error) { 383 return data, nil 384 }