get.porter.sh/porter@v1.3.0/pkg/storage/migrations/migration.go (about) 1 package migrations 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "path/filepath" 8 "sort" 9 "strings" 10 "time" 11 12 "get.porter.sh/porter/pkg/cnab" 13 "get.porter.sh/porter/pkg/config" 14 "get.porter.sh/porter/pkg/plugins/pluggable" 15 "get.porter.sh/porter/pkg/secrets" 16 "get.porter.sh/porter/pkg/storage" 17 "get.porter.sh/porter/pkg/storage/migrations/crudstore" 18 "get.porter.sh/porter/pkg/storage/plugins" 19 "get.porter.sh/porter/pkg/tracing" 20 "github.com/hashicorp/go-multierror" 21 "go.opentelemetry.io/otel/attribute" 22 ) 23 24 // Migration can connect to a legacy Porter v0.38 storage plugin migrate the data 25 // in the specified account into a target account compatible with the current 26 // version of Porter. 27 type Migration struct { 28 config *config.Config 29 opts storage.MigrateOptions 30 sourceStore crudstore.Store 31 destStore storage.Store 32 sanitizer *storage.Sanitizer 33 pluginConn *pluggable.PluginConnection 34 } 35 36 func NewMigration(c *config.Config, opts storage.MigrateOptions, destStore storage.Store, sanitizer *storage.Sanitizer) *Migration { 37 return &Migration{ 38 config: c, 39 opts: opts, 40 destStore: destStore, 41 sanitizer: sanitizer, 42 } 43 } 44 45 // Connect loads the legacy plugin specified by the source storage account. 46 func (m *Migration) Connect(ctx context.Context) error { 47 ctx, log := tracing.StartSpan(ctx, 48 attribute.String("storage-name", m.opts.OldStorageAccount)) 49 defer log.EndSpan() 50 51 // Create a config file that uses the old PORTER_HOME 52 oldConfig := config.New() 53 oldConfig.SetHomeDir(m.opts.OldHome) 54 oldConfig.SetPorterPath(filepath.Join(m.opts.OldHome, "porter")) 55 _, err := oldConfig.Load(ctx, nil) 56 if err != nil { 57 return log.Error(fmt.Errorf("could not load old config: %w", err)) 58 } 59 oldConfig.Setenv(config.EnvHOME, m.opts.OldHome) 60 61 l := pluggable.NewPluginLoader(oldConfig) 62 conn, err := l.Load(ctx, m.legacyStoragePluginConfig()) 63 if err != nil { 64 return log.Error(fmt.Errorf("could not load legacy storage plugin: %w", err)) 65 } 66 m.pluginConn = conn 67 68 connected := false 69 defer func() { 70 if !connected { 71 conn.Close(ctx) 72 } 73 }() 74 75 // Cast the plugin connection to a subset of the old protocol from v0.38 that can only read data 76 store, ok := conn.GetClient().(crudstore.Store) 77 if !ok { 78 return log.Error(fmt.Errorf("the interface exposed by the %s plugin was not crudstore.Store", conn)) 79 } 80 81 m.sourceStore = store 82 connected = true 83 return nil 84 } 85 86 func (m *Migration) legacyStoragePluginConfig() pluggable.PluginTypeConfig { 87 return pluggable.PluginTypeConfig{ 88 Interface: plugins.PluginInterface, 89 Plugin: &crudstore.Plugin{}, 90 GetDefaultPluggable: func(c *config.Config) string { 91 // Load the config for the specific storage account named as the source for the migration 92 return m.opts.OldStorageAccount 93 }, 94 GetPluggable: func(c *config.Config, name string) (pluggable.Entry, error) { 95 return c.GetStorage(name) 96 }, 97 GetDefaultPlugin: func(c *config.Config) string { 98 // filesystem is the default storage plugin for v0.38 99 return "filesystem" 100 }, 101 ProtocolVersion: 1, // protocol version used by porter v0.38 102 } 103 } 104 105 func (m *Migration) Close() error { 106 m.pluginConn.Close(context.Background()) 107 return nil 108 } 109 110 func (m *Migration) Migrate(ctx context.Context) (storage.Schema, error) { 111 ctx, span := tracing.StartSpan(ctx) 112 defer span.EndSpan() 113 114 if err := m.Connect(ctx); err != nil { 115 return storage.Schema{}, err 116 } 117 118 currentSchema, err := m.loadSourceSchema() 119 if err != nil { 120 return storage.Schema{}, err 121 } 122 123 span.SetAttributes( 124 attribute.String("installationSchema", string(currentSchema.Installations)), 125 attribute.String("parameterSchema", string(currentSchema.Parameters)), 126 attribute.String("credentialSchema", string(currentSchema.Credentials)), 127 ) 128 129 // Attempt to migrate all data, don't immediately stop when one fails 130 // Report how it went at the end 131 var migrationErr *multierror.Error 132 if currentSchema.ShouldMigrateInstallations() { 133 span.Info("Installations schema is out-of-date. Migrating...") 134 err = m.migrateInstallations(ctx) 135 migrationErr = multierror.Append(migrationErr, err) 136 } else { 137 span.Info("Installations schema is up-to-date") 138 } 139 140 if currentSchema.ShouldMigrateCredentialSets() { 141 span.Info("Credential Sets schema is out-of-date. Migrating...") 142 err = m.migrateCredentialSets(ctx) 143 migrationErr = multierror.Append(migrationErr, err) 144 } else { 145 span.Info("Credential Sets schema is up-to-date") 146 } 147 148 if currentSchema.ShouldMigrateParameterSets() { 149 span.Info("Parameters schema is out-of-date. Migrating...") 150 err = m.migrateParameterSets(ctx) 151 migrationErr = multierror.Append(migrationErr, err) 152 } else { 153 span.Info("Parameter Sets schema is up-to-date") 154 } 155 156 // Write the updated schema if the migration was successful 157 if migrationErr.ErrorOrNil() == nil { 158 currentSchema, err = WriteSchema(ctx, m.destStore) 159 migrationErr = multierror.Append(migrationErr, err) 160 } 161 162 return currentSchema, migrationErr.ErrorOrNil() 163 } 164 165 func (m *Migration) loadSourceSchema() (storage.Schema, error) { 166 // Load the schema from the old PORTER_HOME 167 schemaData, err := m.sourceStore.Read("", "schema") 168 if err != nil { 169 return storage.Schema{}, fmt.Errorf("error reading the schema from the old PORTER_HOME: %w", err) 170 } 171 172 var srcSchema SourceSchema 173 if err = json.Unmarshal(schemaData, &srcSchema); err != nil { 174 return storage.Schema{}, fmt.Errorf("error parsing the schema from the old PORTER_HOME: %w", err) 175 } 176 177 currentSchema := storage.Schema{ 178 ID: "schema", 179 Installations: srcSchema.Claims, 180 Credentials: srcSchema.Credentials, 181 Parameters: srcSchema.Parameters, 182 } 183 return currentSchema, nil 184 } 185 186 func (m *Migration) migrateInstallations(ctx context.Context) error { 187 ctx, span := tracing.StartSpan(ctx) 188 defer span.EndSpan() 189 190 // Get a list of all the installation names 191 names, err := m.listItems("installations", "") 192 if err != nil { 193 return span.Error(fmt.Errorf("error listing installations from the source account: %w", err)) 194 } 195 196 span.Infof("Found %d installations to migrate", len(names)) 197 198 var bigErr *multierror.Error 199 for _, name := range names { 200 if err = m.migrateInstallation(ctx, name); err != nil { 201 // Keep track of which installations failed but otherwise keep trying to migrate as many as possible 202 bigErr = multierror.Append(bigErr, span.Error(err, attribute.String("installation", name))) 203 } 204 } 205 206 return bigErr.ErrorOrNil() 207 } 208 209 func (m *Migration) migrateInstallation(ctx context.Context, installationName string) error { 210 inst := convertInstallation(installationName) 211 inst.Namespace = m.opts.NewNamespace 212 213 // Find all claims associated with the installation 214 claimIDs, err := m.listItems("claims", installationName) 215 if err != nil { 216 return err 217 } 218 219 for _, claimID := range claimIDs { 220 if err = m.migrateClaim(ctx, &inst, claimID); err != nil { 221 return err 222 } 223 } 224 225 // Sort the claims earliest to latest and assign the installation id 226 // to the earliest claim id. This gives us a consistent installation id when the migration is repeated. 227 sort.Strings(claimIDs) 228 inst.ID = claimIDs[0] 229 230 updateOpts := storage.UpdateOptions{Document: inst, Upsert: true} 231 err = m.destStore.Update(ctx, storage.CollectionInstallations, updateOpts) 232 if err != nil { 233 return fmt.Errorf("error upserting migrated installation %s: %w", inst.Name, err) 234 } 235 236 return nil 237 } 238 239 func convertInstallation(installationName string) storage.Installation { 240 inst := storage.NewInstallation("", installationName) 241 242 // Clear fields that are generated and later we will set them consistently using the claim data 243 inst.ID = "" 244 inst.Status.Created = time.Time{} 245 inst.Status.Modified = time.Time{} 246 247 return inst 248 } 249 250 // migrateClaim migrates the specified claim record into the target database, updating the installation 251 // status based on all processed claims (such as setting the created date for the installation). 252 func (m *Migration) migrateClaim(ctx context.Context, inst *storage.Installation, claimID string) error { 253 // inst is a ref because migrateClaim will update its status based on the processed claims 254 255 ctx, span := tracing.StartSpan(ctx, 256 attribute.String("installation", inst.Name), attribute.String("claimID", claimID)) 257 defer span.EndSpan() 258 259 data, err := m.sourceStore.Read("claims", claimID) 260 if err != nil { 261 return span.Error(err) 262 } 263 264 run, err := convertClaimToRun(*inst, data) 265 if err != nil { 266 return span.Error(err) 267 } 268 269 // Update the installation status based on the run 270 // Use the most early claim timestamp as the creation date of the installation 271 if inst.Status.Created.IsZero() || inst.Status.Created.After(run.Created) { 272 inst.Status.Created = run.Created 273 } 274 275 // Use the most recent claim timestamp as the modified date of the installation 276 if inst.Status.Modified.IsZero() || inst.Status.Modified.Before(run.Created) { 277 inst.Status.Modified = run.Created 278 } 279 280 // Sanitize sensitive values on the source claim 281 bun := cnab.ExtendedBundle{Bundle: run.Bundle} 282 cleanParams, err := m.sanitizer.CleanParameters(ctx, run.Parameters.Parameters, bun, run.ID) 283 if err != nil { 284 return span.Error(err) 285 } 286 run.Parameters.Parameters = cleanParams 287 288 // Find all results associated with the run 289 resultIDs, err := m.listItems("results", run.ID) 290 if err != nil { 291 return err 292 } 293 294 for _, resultID := range resultIDs { 295 if err = m.migrateResult(ctx, inst, run, resultID); err != nil { 296 return err 297 } 298 } 299 300 updateOpts := storage.UpdateOptions{Document: run, Upsert: true} 301 err = m.destStore.Update(ctx, storage.CollectionRuns, updateOpts) 302 if err != nil { 303 return span.Error(err) 304 } 305 306 return nil 307 } 308 309 func convertClaimToRun(inst storage.Installation, data []byte) (storage.Run, error) { 310 var src SourceClaim 311 if err := json.Unmarshal(data, &src); err != nil { 312 return storage.Run{}, fmt.Errorf("error parsing claim record: %w", err) 313 } 314 315 params := make([]secrets.SourceMap, 0, len(src.Parameters)) 316 for k, v := range src.Parameters { 317 stringVal, err := cnab.WriteParameterToString(k, v) 318 if err != nil { 319 return storage.Run{}, err 320 } 321 params = append(params, storage.ValueStrategy(k, stringVal)) 322 } 323 324 dest := storage.Run{ 325 SchemaVersion: storage.DefaultInstallationSchemaVersion, 326 ID: src.ID, 327 Created: src.Created, 328 Namespace: inst.Namespace, 329 Installation: src.Installation, 330 Revision: src.Revision, 331 Action: src.Action, 332 Bundle: src.Bundle, 333 BundleReference: src.BundleReference, 334 BundleDigest: "", // We didn't track digest before v1 335 Parameters: storage.NewInternalParameterSet(inst.Namespace, src.ID, params...), 336 Custom: src.Custom, 337 ParametersDigest: "", // Leave blank, and Porter will re-resolve later if needed. This is just a cached value to improve performance. 338 } 339 340 return dest, nil 341 } 342 343 func (m *Migration) migrateResult(ctx context.Context, inst *storage.Installation, run storage.Run, resultID string) error { 344 // inst is a ref because migrateResult will update the installation status based on the result of the run 345 346 ctx, span := tracing.StartSpan(ctx, attribute.String("resultID", resultID)) 347 defer span.EndSpan() 348 349 data, err := m.sourceStore.Read("results", resultID) 350 if err != nil { 351 return span.Error(err) 352 } 353 354 result, err := convertResult(run, data) 355 if err != nil { 356 return span.Error(err) 357 } 358 359 updateOpts := storage.UpdateOptions{Document: result, Upsert: true} 360 err = m.destStore.Update(ctx, storage.CollectionResults, updateOpts) 361 if err != nil { 362 return span.Error(err) 363 } 364 365 // Update the installation status based on the result of previous runs 366 inst.ApplyResult(run, result) 367 368 // Find all outputs associated with the result 369 outputKeys, err := m.listItems("outputs", resultID) 370 if err != nil { 371 return err 372 } 373 374 for _, outputKey := range outputKeys { 375 if err = m.migrateOutput(ctx, run, result, outputKey); err != nil { 376 return err 377 } 378 } 379 380 return nil 381 } 382 383 func convertResult(run storage.Run, data []byte) (storage.Result, error) { 384 var src SourceResult 385 if err := json.Unmarshal(data, &src); err != nil { 386 return storage.Result{}, fmt.Errorf("error parsing result record: %w", err) 387 } 388 389 dest := storage.Result{ 390 SchemaVersion: run.SchemaVersion, 391 ID: src.ID, 392 Created: src.Created, 393 Namespace: run.Namespace, 394 Installation: run.Installation, 395 RunID: run.ID, 396 Message: src.Message, 397 Status: src.Status, 398 OutputMetadata: src.OutputMetadata, 399 Custom: src.Custom, 400 } 401 402 return dest, nil 403 } 404 405 func (m *Migration) migrateOutput(ctx context.Context, run storage.Run, result storage.Result, outputKey string) error { 406 ctx, span := tracing.StartSpan(ctx, attribute.String("outputKey", outputKey)) 407 defer span.EndSpan() 408 409 data, err := m.sourceStore.Read("outputs", outputKey) 410 if err != nil { 411 return span.Error(err) 412 } 413 414 output, err := convertOutput(result, outputKey, data) 415 if err != nil { 416 return span.Error(err) 417 } 418 419 // Sanitize sensitive outputs 420 bun := cnab.ExtendedBundle{Bundle: run.Bundle} 421 output, err = m.sanitizer.CleanOutput(ctx, output, bun) 422 if err != nil { 423 return span.Error(err) 424 } 425 426 updateOpts := storage.UpdateOptions{Document: output, Upsert: true} 427 err = m.destStore.Update(ctx, storage.CollectionOutputs, updateOpts) 428 if err != nil { 429 return span.Error(fmt.Errorf("error upserting migrated output %s: %w", outputKey, err)) 430 } 431 432 return nil 433 } 434 435 func convertOutput(result storage.Result, outputKey string, data []byte) (storage.Output, error) { 436 _, outputName, ok := strings.Cut(outputKey, "-") 437 if !ok { 438 return storage.Output{}, fmt.Errorf("error converting source output: invalid output key %s", outputKey) 439 } 440 441 dest := storage.Output{ 442 SchemaVersion: result.SchemaVersion, 443 Name: outputName, 444 Namespace: result.Namespace, 445 Installation: result.Installation, 446 RunID: result.RunID, 447 ResultID: result.ID, 448 Value: data, 449 } 450 451 return dest, nil 452 } 453 454 func (m *Migration) migrateCredentialSets(ctx context.Context) error { 455 ctx, span := tracing.StartSpan(ctx) 456 defer span.EndSpan() 457 458 // Get a list of all the credential set names 459 names, err := m.listItems("credentials", "") 460 if err != nil { 461 return span.Error(fmt.Errorf("error listing credential sets from the source account: %w", err)) 462 } 463 464 span.Infof("Found %d credential sets to migrate", len(names)) 465 466 var bigErr *multierror.Error 467 for _, name := range names { 468 if err = m.migrateCredentialSet(ctx, name); err != nil { 469 // Keep track of which ones failed but otherwise keep trying to migrate as many as possible 470 bigErr = multierror.Append(bigErr, err) 471 } 472 } 473 474 return bigErr.ErrorOrNil() 475 } 476 477 func (m *Migration) migrateCredentialSet(ctx context.Context, name string) error { 478 ctx, span := tracing.StartSpan(ctx, attribute.String("credential-set", name)) 479 defer span.EndSpan() 480 481 data, err := m.sourceStore.Read("credentials", name) 482 if err != nil { 483 return span.Error(err) 484 } 485 486 dest, err := convertCredentialSet(m.opts.NewNamespace, data) 487 if err != nil { 488 return span.Error(err) 489 } 490 491 updateOpts := storage.UpdateOptions{Document: dest, Upsert: true} 492 err = m.destStore.Update(ctx, storage.CollectionCredentials, updateOpts) 493 if err != nil { 494 return span.Error(fmt.Errorf("error upserting migrated credential set %s: %w", name, err)) 495 } 496 497 return nil 498 } 499 500 func convertCredentialSet(namespace string, data []byte) (storage.CredentialSet, error) { 501 var src SourceCredentialSet 502 if err := json.Unmarshal(data, &src); err != nil { 503 return storage.CredentialSet{}, fmt.Errorf("error parsing credential set record: %w", err) 504 } 505 506 dest := storage.CredentialSet{ 507 CredentialSetSpec: storage.CredentialSetSpec{ 508 SchemaVersion: storage.DefaultCredentialSetSchemaVersion, 509 Namespace: namespace, 510 Name: src.Name, 511 Credentials: make([]secrets.SourceMap, len(src.Credentials)), 512 }, 513 Status: storage.CredentialSetStatus{ 514 Created: src.Created, 515 Modified: src.Modified, 516 }, 517 } 518 519 for i, cred := range src.Credentials { 520 dest.CredentialSetSpec.Credentials[i] = secrets.SourceMap{ 521 Name: cred.Name, 522 Source: secrets.Source{ 523 Strategy: cred.Source.Key, 524 Hint: cred.Source.Value, 525 }, 526 } 527 } 528 529 return dest, nil 530 } 531 532 func (m *Migration) migrateParameterSets(ctx context.Context) error { 533 ctx, span := tracing.StartSpan(ctx) 534 defer span.EndSpan() 535 536 // Get a list of all the parameter set names 537 names, err := m.listItems("parameters", "") 538 if err != nil { 539 return span.Error(fmt.Errorf("error listing credential sets from the source account: %w", err)) 540 } 541 542 span.Infof("Found %d parameter sets to migrate", len(names)) 543 544 var bigErr *multierror.Error 545 for _, name := range names { 546 if err = m.migrateParameterSet(ctx, name); err != nil { 547 // Keep track of which ones failed but otherwise keep trying to migrate as many as possible 548 bigErr = multierror.Append(bigErr, err) 549 } 550 } 551 552 return bigErr.ErrorOrNil() 553 } 554 555 func (m *Migration) migrateParameterSet(ctx context.Context, name string) error { 556 ctx, span := tracing.StartSpan(ctx, attribute.String("credential-set", name)) 557 defer span.EndSpan() 558 559 data, err := m.sourceStore.Read("parameters", name) 560 if err != nil { 561 return span.Error(err) 562 } 563 564 dest, err := convertParameterSet(m.opts.NewNamespace, data) 565 if err != nil { 566 return span.Error(err) 567 } 568 569 updateOpts := storage.UpdateOptions{Document: dest, Upsert: true} 570 err = m.destStore.Update(ctx, storage.CollectionParameters, updateOpts) 571 if err != nil { 572 return span.Error(fmt.Errorf("error upserting migrated credential set %s: %w", name, err)) 573 } 574 575 return nil 576 } 577 578 func convertParameterSet(namespace string, data []byte) (storage.ParameterSet, error) { 579 var src SourceParameterSet 580 if err := json.Unmarshal(data, &src); err != nil { 581 return storage.ParameterSet{}, fmt.Errorf("error parsing parameter set record: %w", err) 582 } 583 584 dest := storage.ParameterSet{ 585 ParameterSetSpec: storage.ParameterSetSpec{ 586 SchemaVersion: storage.DefaultParameterSetSchemaVersion, 587 Namespace: namespace, 588 Name: src.Name, 589 Parameters: make([]secrets.SourceMap, len(src.Parameters)), 590 }, 591 Status: storage.ParameterSetStatus{ 592 Created: src.Created, 593 Modified: src.Modified, 594 }, 595 } 596 597 for i, cred := range src.Parameters { 598 dest.Parameters[i] = secrets.SourceMap{ 599 Name: cred.Name, 600 Source: secrets.Source{ 601 Strategy: cred.Source.Key, 602 Hint: cred.Source.Value, 603 }, 604 } 605 } 606 607 return dest, nil 608 } 609 610 // List items in a collection, and safely handles when there are no results 611 func (m *Migration) listItems(itemType string, group string) ([]string, error) { 612 names, err := m.sourceStore.List(itemType, group) 613 if err != nil { 614 // Check for a sentinel error that was returned from legacy plugins 615 // when it couldn't list data because the container for the item or group didn't exist 616 // This just means no items were found. 617 if strings.Contains(err.Error(), "File does not exist") { 618 return nil, nil 619 } 620 621 return nil, err 622 } 623 624 return names, nil 625 }