get.porter.sh/porter@v1.3.0/pkg/storage/migrations/manager.go (about)

     1  package migrations
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  
     8  	"get.porter.sh/porter/pkg"
     9  	"get.porter.sh/porter/pkg/config"
    10  	"get.porter.sh/porter/pkg/storage"
    11  	"get.porter.sh/porter/pkg/tracing"
    12  )
    13  
    14  const (
    15  	// CollectionConfig is the collection that stores Porter configuration documents
    16  	// such as the storage schema.
    17  	CollectionConfig = "config"
    18  )
    19  
    20  var _ storage.Store = &storage.PluginAdapter{}
    21  var _ storage.Store = &Manager{}
    22  
    23  // Manager handles high level functions over Porter's storage systems such as
    24  // migrating data formats.
    25  type Manager struct {
    26  	*config.Config
    27  
    28  	// The underlying storage managed by this instance. It
    29  	// shouldn't be used for typed read/access the data, for that storage.InstallationStorageProvider
    30  	// or storage.CredentialSetProvider which works with the Manager.
    31  	store storage.Store
    32  
    33  	// initialized specifies if we have loaded the schema document.
    34  	initialized bool
    35  
    36  	// schema document that defines the current version of each storage system.
    37  	// We use this to detect when we are out-of-date and require a migration.
    38  	schema storage.Schema
    39  
    40  	// Allow the schema to be out-of-date, defaults to false. Prevents
    41  	// connections to underlying storage when the schema is out-of-date
    42  	allowOutOfDateSchema bool
    43  
    44  	// Cleans sensitive data so that we don't store it in our database
    45  	// This is set by Initialize not the constructor due to a bit of a circular reference between Manager, ParameterStore, SecretStore and Sanitizer.
    46  	// Improvements could most definitely be made here!
    47  	sanitizer *storage.Sanitizer
    48  }
    49  
    50  // NewManager creates a storage manager for a backing datastore.
    51  func NewManager(c *config.Config, storage storage.Store) *Manager {
    52  	mgr := &Manager{
    53  		Config: c,
    54  		store:  storage,
    55  		// We can't set sanitizer here yet, it is set in Initialize
    56  	}
    57  
    58  	return mgr
    59  }
    60  
    61  // Initialize configures the storage manager with additional configuration that wasn't available
    62  // when the manager instance was created.
    63  func (m *Manager) Initialize(sanitizer *storage.Sanitizer) {
    64  	m.sanitizer = sanitizer
    65  }
    66  
    67  // Connect initializes storage manager for use.
    68  // The manager itself is responsible for ensuring it was called.
    69  // Close is called automatically when the manager is used by Porter.
    70  func (m *Manager) Connect(ctx context.Context) error {
    71  	ctx, span := tracing.StartSpan(ctx)
    72  	defer span.EndSpan()
    73  
    74  	if !m.initialized {
    75  		span.Debug("Checking database schema")
    76  
    77  		if err := m.loadSchema(ctx); err != nil {
    78  			return err
    79  		}
    80  
    81  		if !m.allowOutOfDateSchema && m.schema.IsOutOfDate() {
    82  			m.Close()
    83  			return span.Error(fmt.Errorf(`The schema of Porter's data is in an older format than supported by this version of Porter. 
    84  
    85  Porter %s uses the following database schema:
    86  
    87  %#v
    88  
    89  Your database schema is:
    90  
    91  %#v
    92  
    93  Refer to https://porter.sh/storage-migrate for more information and instructions to back up your data. 
    94  Once your data has been backed up, run the following command to perform the migration:
    95  
    96      porter storage migrate
    97  `, pkg.Version, storage.NewSchema(), m.schema))
    98  		}
    99  		m.initialized = true
   100  
   101  		err := storage.EnsureInstallationIndices(ctx, m.store)
   102  		if err != nil {
   103  			return err
   104  		}
   105  
   106  		err = storage.EnsureParameterIndices(ctx, m.store)
   107  		if err != nil {
   108  			return err
   109  		}
   110  
   111  		err = storage.EnsureCredentialIndices(ctx, m.store)
   112  		if err != nil {
   113  			return err
   114  		}
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  func (m *Manager) Close() error {
   121  	m.store.Close()
   122  	m.initialized = false
   123  	return nil
   124  }
   125  
   126  func (m *Manager) Aggregate(ctx context.Context, collection string, opts storage.AggregateOptions, out interface{}) error {
   127  	if err := m.Connect(ctx); err != nil {
   128  		return err
   129  	}
   130  	return m.store.Aggregate(ctx, collection, opts, out)
   131  }
   132  
   133  func (m *Manager) Count(ctx context.Context, collection string, opts storage.CountOptions) (int64, error) {
   134  	if err := m.Connect(ctx); err != nil {
   135  		return 0, err
   136  	}
   137  	return m.store.Count(ctx, collection, opts)
   138  }
   139  
   140  func (m *Manager) EnsureIndex(ctx context.Context, opts storage.EnsureIndexOptions) error {
   141  	if err := m.Connect(ctx); err != nil {
   142  		return err
   143  	}
   144  	return m.store.EnsureIndex(ctx, opts)
   145  }
   146  
   147  func (m *Manager) Find(ctx context.Context, collection string, opts storage.FindOptions, out interface{}) error {
   148  	if err := m.Connect(ctx); err != nil {
   149  		return err
   150  	}
   151  	return m.store.Find(ctx, collection, opts, out)
   152  }
   153  
   154  func (m *Manager) FindOne(ctx context.Context, collection string, opts storage.FindOptions, out interface{}) error {
   155  	if err := m.Connect(ctx); err != nil {
   156  		return err
   157  	}
   158  	return m.store.FindOne(ctx, collection, opts, out)
   159  }
   160  
   161  func (m *Manager) Get(ctx context.Context, collection string, opts storage.GetOptions, out interface{}) error {
   162  	if err := m.Connect(ctx); err != nil {
   163  		return err
   164  	}
   165  	return m.store.Get(ctx, collection, opts, out)
   166  }
   167  
   168  func (m *Manager) Insert(ctx context.Context, collection string, opts storage.InsertOptions) error {
   169  	if err := m.Connect(ctx); err != nil {
   170  		return err
   171  	}
   172  	return m.store.Insert(ctx, collection, opts)
   173  }
   174  
   175  func (m *Manager) Patch(ctx context.Context, collection string, opts storage.PatchOptions) error {
   176  	if err := m.Connect(ctx); err != nil {
   177  		return err
   178  	}
   179  	return m.store.Patch(ctx, collection, opts)
   180  }
   181  
   182  func (m *Manager) Remove(ctx context.Context, collection string, opts storage.RemoveOptions) error {
   183  	if err := m.Connect(ctx); err != nil {
   184  		return err
   185  	}
   186  	return m.store.Remove(ctx, collection, opts)
   187  }
   188  
   189  func (m *Manager) Update(ctx context.Context, collection string, opts storage.UpdateOptions) error {
   190  	if err := m.Connect(ctx); err != nil {
   191  		return err
   192  	}
   193  	return m.store.Update(ctx, collection, opts)
   194  }
   195  
   196  // loadSchema parses the schema file at the root of PORTER_HOME. This file (when present) contains
   197  // a list of the current version of each of Porter's storage systems.
   198  func (m *Manager) loadSchema(ctx context.Context) error {
   199  	var schema storage.Schema
   200  	err := m.store.Get(ctx, CollectionConfig, storage.GetOptions{ID: "schema"}, &schema)
   201  	if err != nil {
   202  		if errors.Is(err, storage.ErrNotFound{}) {
   203  			emptyHome, err := m.initEmptyPorterHome(ctx)
   204  			if emptyHome {
   205  				// Return any errors from creating a schema document in an empty porter home directory
   206  				return err
   207  			} else {
   208  				// When we don't have an empty home directory, and we can't find the schema
   209  				// document, we need to do a migration
   210  				return nil
   211  			}
   212  		}
   213  		return fmt.Errorf("could not read storage schema document: %w", err)
   214  	}
   215  
   216  	m.schema = schema
   217  
   218  	if err != nil {
   219  		return fmt.Errorf("could not parse storage schema document: %w", err)
   220  	}
   221  
   222  	return nil
   223  }
   224  
   225  // Migrate executes a migration on any/all of Porter's storage sub-systems.
   226  // You must call Initialize before calling Migrate.
   227  func (m *Manager) Migrate(ctx context.Context, opts storage.MigrateOptions) error {
   228  	if m.sanitizer == nil {
   229  		return fmt.Errorf("cannot call storage.Manager.Migrate before calling Initialize and passing a storage.Sanitizer")
   230  	}
   231  
   232  	m.reset()
   233  
   234  	// Let us call connect and not have it kick us out because the schema is out-of-date
   235  	m.allowOutOfDateSchema = true
   236  	defer func() {
   237  		m.allowOutOfDateSchema = false
   238  	}()
   239  
   240  	// Reuse the same connection for the entire migration
   241  	err := m.Connect(ctx)
   242  	if err != nil {
   243  		return err
   244  	}
   245  	defer m.Close()
   246  
   247  	migration := NewMigration(m.Config, opts, m.store, m.sanitizer)
   248  	defer migration.Close()
   249  
   250  	newSchema, err := migration.Migrate(ctx)
   251  	if err != nil {
   252  		return err
   253  	}
   254  
   255  	m.schema = newSchema
   256  	return nil
   257  }
   258  
   259  // When there is no schema, and no existing storage data, create an initial
   260  // schema file and allow the operation to continue. Don't require a
   261  // migration.
   262  func (m *Manager) initEmptyPorterHome(ctx context.Context) (bool, error) {
   263  	if m.schema != (storage.Schema{}) {
   264  		return false, nil
   265  	}
   266  
   267  	itemCheck := func(itemType string) (bool, error) {
   268  		itemCount, err := m.store.Count(ctx, itemType, storage.CountOptions{})
   269  		if err != nil {
   270  			return false, fmt.Errorf("error checking for existing %s when checking if PORTER_HOME is new: %w", itemType, err)
   271  		}
   272  
   273  		return itemCount > 0, nil
   274  	}
   275  
   276  	hasInstallations, err := itemCheck(storage.CollectionInstallations)
   277  	if hasInstallations || err != nil {
   278  		return false, err
   279  	}
   280  
   281  	hasCredentials, err := itemCheck(storage.CollectionCredentials)
   282  	if hasCredentials || err != nil {
   283  		return false, err
   284  	}
   285  
   286  	hasParameters, err := itemCheck(storage.CollectionParameters)
   287  	if hasParameters || err != nil {
   288  		return false, err
   289  	}
   290  
   291  	return true, m.WriteSchema(ctx)
   292  }
   293  
   294  // reset allows us to relook at our schema.json even after it has been read.
   295  func (m *Manager) reset() {
   296  	m.schema = storage.Schema{}
   297  	m.initialized = false
   298  }
   299  
   300  // WriteSchema updates the database to indicate that it conforms with the current database schema.
   301  func (m *Manager) WriteSchema(ctx context.Context) error {
   302  	var err error
   303  	m.schema, err = WriteSchema(ctx, m.store)
   304  	return err
   305  }
   306  
   307  // WriteSchema updates the database to indicate that it conforms with the current database schema.
   308  func WriteSchema(ctx context.Context, store storage.Store) (storage.Schema, error) {
   309  	schema := storage.NewSchema()
   310  
   311  	err := store.Update(ctx, CollectionConfig, storage.UpdateOptions{Document: schema, Upsert: true})
   312  	if err != nil {
   313  		return storage.Schema{}, fmt.Errorf("Unable to save storage schema file to the database: %w", err)
   314  	}
   315  
   316  	return schema, nil
   317  }