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 }