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  }