github.com/mendersoftware/go-lib-micro@v0.0.0-20240304135804-e8e39c59b148/mongo/migrate/migrator_simple.go (about)

     1  // Copyright 2023 Northern.tech AS
     2  //
     3  //	Licensed under the Apache License, Version 2.0 (the "License");
     4  //	you may not use this file except in compliance with the License.
     5  //	You may obtain a copy of the License at
     6  //
     7  //	    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  //	Unless required by applicable law or agreed to in writing, software
    10  //	distributed under the License is distributed on an "AS IS" BASIS,
    11  //	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  //	See the License for the specific language governing permissions and
    13  //	limitations under the License.
    14  package migrate
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"sort"
    20  	"strings"
    21  
    22  	"github.com/pkg/errors"
    23  	"go.mongodb.org/mongo-driver/mongo"
    24  
    25  	"github.com/mendersoftware/go-lib-micro/log"
    26  )
    27  
    28  var (
    29  	ErrNeedsMigration = "db needs migration"
    30  )
    31  
    32  func IsErrNeedsMigration(e error) bool {
    33  	return strings.HasPrefix(e.Error(), ErrNeedsMigration)
    34  }
    35  
    36  // SimpleMigratior applies migrations by comparing `Version` of migrations
    37  // passed to Apply() and already applied migrations. Only migrations that are of
    38  // version higher than the last applied migration will be run. For example:
    39  //
    40  //	already applied migrations: 1.0.0, 1.0.1, 1.0.2
    41  //	migrations in Apply(): 1.0.1, 1.0.3, 1.1.0
    42  //	migrations that will be applied: 1.0.3, 1.1.0
    43  type SimpleMigrator struct {
    44  	Client      *mongo.Client
    45  	Db          string
    46  	Automigrate bool
    47  }
    48  
    49  // Apply will apply migrations, provided that Automigrate is on. After each successful migration a new migration
    50  // record will be added to DB with the version of migration that was just
    51  // applied. If a migration fails, Apply() returns an error and does not add a
    52  // migration record (so last migration that is recorded is N-1).
    53  //
    54  // Apply() will log some messages when running. Logger will be extracted from
    55  // context using go-lib-micro/log.LoggerContextKey as key.
    56  // If Automigrate is off, the migrator will just check if the DB is up-to-date, and return with ErrNeedsMigration otherwise.
    57  // Check for it with IsErrNeedsMigration.
    58  func (m *SimpleMigrator) Apply(ctx context.Context, target Version, migrations []Migration) error {
    59  	l := log.FromContext(ctx).F(log.Ctx{"db": m.Db})
    60  
    61  	sort.Slice(migrations, func(i int, j int) bool {
    62  		return VersionIsLess(migrations[i].Version(), migrations[j].Version())
    63  	})
    64  
    65  	applied, err := GetMigrationInfo(ctx, m.Client, m.Db)
    66  	if err != nil {
    67  		return errors.Wrap(err, "failed to list applied migrations")
    68  	}
    69  
    70  	// starts at 0.0.0
    71  	last := Version{}
    72  
    73  	if len(applied) != 0 {
    74  		// sort applied migrations wrt. version
    75  		sort.Slice(applied, func(i int, j int) bool {
    76  			return VersionIsLess(applied[i].Version, applied[j].Version)
    77  		})
    78  		// last version from already applied migrations
    79  		last = applied[len(applied)-1].Version
    80  	}
    81  
    82  	// if Automigrate is disabled - just check
    83  	// if the last applied migration is lower than the target one
    84  	if !m.Automigrate {
    85  		if VersionIsLess(last, target) {
    86  			return fmt.Errorf(ErrNeedsMigration+": %s has version %s, needs version %s", m.Db, last.String(), target.String())
    87  		} else {
    88  			return nil
    89  		}
    90  	}
    91  
    92  	// try to apply migrations
    93  	for _, migration := range migrations {
    94  		mv := migration.Version()
    95  		if VersionIsLess(target, mv) {
    96  			l.Warnf("migration to version %s skipped, target version %s is lower",
    97  				mv, target)
    98  		} else if VersionIsLess(last, mv) {
    99  			// log, migration applied
   100  			l.Infof("applying migration from version %s to %s",
   101  				last, mv)
   102  
   103  			// apply migration
   104  			if err := migration.Up(last); err != nil {
   105  				l.Errorf("migration from %s to %s failed: %s",
   106  					last, mv, err)
   107  
   108  				// migration from last to migration.Version() failed: err
   109  				return errors.Wrapf(err,
   110  					"failed to apply migration from %s to %s",
   111  					last, mv)
   112  			}
   113  
   114  			if err := UpdateMigrationInfo(ctx, mv, m.Client, m.Db); err != nil {
   115  
   116  				return errors.Wrapf(err,
   117  					"failed to record migration from %s to %s",
   118  					last, mv)
   119  
   120  			}
   121  			last = mv
   122  		} else {
   123  			// log migration already applied
   124  			l.Infof("migration to version %s skipped", mv)
   125  		}
   126  	}
   127  
   128  	// ideally, when all migrations have completed, DB should be in `target` version
   129  	if VersionIsLess(last, target) {
   130  		l.Warnf("last migration to version %s did not produce target version %s",
   131  			last, target)
   132  		// record DB version anyways
   133  		if err := UpdateMigrationInfo(ctx, target, m.Client, m.Db); err != nil {
   134  			return errors.Wrapf(err,
   135  				"failed to record migration from %s to %s",
   136  				last, target)
   137  		}
   138  	} else {
   139  		l.Infof("DB migrated to version %s", target)
   140  	}
   141  
   142  	return nil
   143  }