github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/config/migrate/migrate.go (about)

     1  // Package migrate defines migrations for qri configuration files
     2  package migrate
     3  
     4  import (
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	logging "github.com/ipfs/go-log"
    16  	"github.com/mitchellh/go-homedir"
    17  	"github.com/qri-io/ioes"
    18  	"github.com/qri-io/qfs/qipfs"
    19  	"github.com/qri-io/qri/config"
    20  	qerr "github.com/qri-io/qri/errors"
    21  	"github.com/qri-io/qri/repo"
    22  	"github.com/qri-io/qri/repo/buildrepo"
    23  )
    24  
    25  var (
    26  	log = logging.Logger("migrate")
    27  	// ErrNeedMigration indicates a migration is required
    28  	ErrNeedMigration = fmt.Errorf("migration required")
    29  	// ErrMigrationSucceeded indicates a migration completed executing
    30  	ErrMigrationSucceeded = errors.New("migration succeeded")
    31  )
    32  
    33  // RunMigrations executes migrations. if a migration is required, the shouldRun
    34  // func is called, and exits without migrating if shouldRun returns false.
    35  // if errorOnSuccess is true, a completed migration will return
    36  // ErrMigrationSucceeded instead of nil
    37  func RunMigrations(streams ioes.IOStreams, cfg *config.Config, shouldRun func() bool, errorOnSuccess bool) (err error) {
    38  	if cfg.Revision != config.CurrentConfigRevision {
    39  		if !shouldRun() {
    40  			return qerr.New(ErrNeedMigration, `your repo requires migration before it can run`)
    41  		}
    42  
    43  		streams.PrintErr("migrating configuration...\n")
    44  		if cfg.Revision == 0 {
    45  			if err := ZeroToOne(cfg); err != nil {
    46  				return err
    47  			}
    48  		}
    49  		if cfg.Revision == 1 {
    50  			if err := OneToTwo(cfg); err != nil {
    51  				return err
    52  			}
    53  		}
    54  		if cfg.Revision == 2 {
    55  			if err := TwoToThree(cfg); err != nil {
    56  				return err
    57  			}
    58  		}
    59  		if cfg.Revision == 3 {
    60  			if err := ThreeToFour(cfg); err != nil {
    61  				return err
    62  			}
    63  		}
    64  		streams.PrintErr("done!\n")
    65  
    66  		if errorOnSuccess {
    67  			return ErrMigrationSucceeded
    68  		}
    69  	}
    70  	return nil
    71  }
    72  
    73  // ZeroToOne migrates a configuration from Revision Zero (no revision number) to Revision 1
    74  func ZeroToOne(cfg *config.Config) error {
    75  	if cfg.P2P != nil {
    76  		removes := map[string]bool{
    77  			"/ip4/130.211.198.23/tcp/4001/ipfs/QmNX9nSos8sRFvqGTwdEme6LQ8R1eJ8EuFgW32F9jjp2Pb": true, // mojo
    78  			"/ip4/35.193.162.149/tcp/4001/ipfs/QmTZxETL4YCCzB1yFx4GT1te68henVHD1XPQMkHZ1N22mm": true, // epa
    79  			"/ip4/35.226.92.45/tcp/4001/ipfs/QmP6sbnHXANXgQ7JeCCeCKdJrgpvUd8s75YNfzdkHf6Mpi":   true, // 538
    80  			"/ip4/35.192.140.245/tcp/4001/ipfs/QmUUVNiTz2K9zQSH9PxerKWXmN1p3DBo3oJXurvYziFzqh": true, // EDGI
    81  		}
    82  
    83  		adds := config.DefaultP2P().QriBootstrapAddrs
    84  
    85  		for i, addr := range cfg.P2P.QriBootstrapAddrs {
    86  			// remove any old, invalid addresses
    87  			if removes[addr] {
    88  				cfg.P2P.QriBootstrapAddrs = delIdx(i, cfg.P2P.QriBootstrapAddrs)
    89  			}
    90  			// remove address from list of additions if already configured
    91  			for j, add := range adds {
    92  				if addr == add {
    93  					// adds = append(adds[:j], adds[j+1:]...)
    94  					adds = delIdx(j, adds)
    95  				}
    96  			}
    97  		}
    98  
    99  		cfg.P2P.QriBootstrapAddrs = append(cfg.P2P.QriBootstrapAddrs, adds...)
   100  	}
   101  
   102  	cfg.Revision = 1
   103  
   104  	if err := safeWriteConfig(cfg); err != nil {
   105  		rollbackConfigWrite(cfg)
   106  		return err
   107  	}
   108  
   109  	return nil
   110  }
   111  
   112  // OneToTwo migrates a configuration from Revision 1 to Revision 2
   113  func OneToTwo(cfg *config.Config) error {
   114  	log.Debug("migrating from version one to two")
   115  	qriPath := filepath.Dir(cfg.Path())
   116  	newIPFSPath := filepath.Join(qriPath, "ipfs")
   117  	oldIPFSPath := configVersionOneIPFSPath()
   118  
   119  	if err := qipfs.InternalizeIPFSRepo(oldIPFSPath, newIPFSPath); err != nil {
   120  		return err
   121  	}
   122  
   123  	if err := oneToTwoConfig(cfg); err != nil {
   124  		return err
   125  	}
   126  	cfg.Revision = 2
   127  	if err := cfg.Validate(); err != nil {
   128  		return qerr.New(err, "config is invalid")
   129  	}
   130  	if err := safeWriteConfig(cfg); err != nil {
   131  		rollbackConfigWrite(cfg)
   132  		return err
   133  	}
   134  
   135  	if err := maybeRemoveIPFSRepo(cfg, oldIPFSPath); err != nil {
   136  		log.Debug(err)
   137  		fmt.Printf("error removing IPFS repo at %q:\n\t%s", oldIPFSPath, err)
   138  		fmt.Printf(`qri has successfully internalized this IPFS repo, and no longer 
   139  		needs the folder at %q. you may want to remove it
   140  `, oldIPFSPath)
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  func oneToTwoConfig(cfg *config.Config) error {
   147  	if cfg.API != nil {
   148  		apiCfg := cfg.API
   149  		defaultAPICfg := config.DefaultAPI()
   150  		if apiCfg.Address == "" {
   151  			apiCfg.Address = defaultAPICfg.Address
   152  		}
   153  		// TODO(b5): need a strategy for setting config now that this field is removed
   154  		// in config revision 4
   155  		// if apiCfg.WebsocketAddress == "" {
   156  		// 	apiCfg.WebsocketAddress = defaultAPICfg.WebsocketAddress
   157  		// }
   158  	} else {
   159  		return qerr.New(fmt.Errorf("invalid config"), "config does not contain API configuration")
   160  	}
   161  
   162  	// TODO(b5): need a strategy for setting config now that this field is removed
   163  	// in config revision 4
   164  	// if cfg.RPC != nil {
   165  	// 	defaultRPCCfg := config.DefaultRPC()
   166  	// 	if cfg.RPC.Address == "" {
   167  	// 		cfg.RPC.Address = defaultRPCCfg.Address
   168  	// 	}
   169  	// } else {
   170  	// 	return qerr.New(fmt.Errorf("invalid config"), "config does not contain RPC configuration")
   171  	// }
   172  
   173  	cfg.Filesystems = config.DefaultFilesystems()
   174  
   175  	return nil
   176  }
   177  
   178  // TwoToThree migrates a configuration from Revision 2 to Revision 3
   179  func TwoToThree(cfg *config.Config) error {
   180  	log.Debugf("migrating from revision 2 to 3")
   181  	if cfg.P2P != nil {
   182  		removes := map[string]bool{
   183  			"/ip4/35.239.80.82/tcp/4001/ipfs/QmdpGkbqDYRPCcwLYnEm8oYGz2G9aUZn9WwPjqvqw3XUAc":   true, // red
   184  			"/ip4/35.225.152.38/tcp/4001/ipfs/QmTRqTLbKndFC2rp6VzpyApxHCLrFV35setF1DQZaRWPVf":  true, // orange
   185  			"/ip4/35.202.155.225/tcp/4001/ipfs/QmegNYmwHUQFc3v3eemsYUVf3WiSg4RcMrh3hovA5LncJ2": true, // yellow
   186  			"/ip4/35.238.10.180/tcp/4001/ipfs/QmessbA6uGLJ7HTwbUJ2niE49WbdPfzi27tdYXdAaGRB4G":  true, // green
   187  			"/ip4/35.238.105.35/tcp/4001/ipfs/Qmc353gHY5Wx5iHKHPYj3QDqHP4hVA1MpoSsT6hwSyVx3r":  true, // blue
   188  			"/ip4/35.239.138.186/tcp/4001/ipfs/QmT9YHJF2YkysLqWhhiVTL5526VFtavic3bVueF9rCsjVi": true, // indigo
   189  			"/ip4/35.226.44.58/tcp/4001/ipfs/QmQS2ryqZrjJtPKDy9VTkdPwdUSpTi1TdpGUaqAVwfxcNh":   true, // violet
   190  		}
   191  
   192  		adds := map[string]bool{}
   193  		for _, addr := range config.DefaultP2P().QriBootstrapAddrs {
   194  			adds[addr] = true
   195  		}
   196  
   197  		res := []string{}
   198  
   199  		for _, addr := range cfg.P2P.QriBootstrapAddrs {
   200  			if removes[addr] || adds[addr] {
   201  				continue
   202  			}
   203  			res = append(res, addr)
   204  		}
   205  
   206  		res = append(res, config.DefaultP2P().QriBootstrapAddrs...)
   207  
   208  		cfg.P2P.QriBootstrapAddrs = res
   209  	}
   210  
   211  	cfg.Revision = 3
   212  
   213  	if err := safeWriteConfig(cfg); err != nil {
   214  		rollbackConfigWrite(cfg)
   215  		return err
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  // ThreeToFour migrates a configuration from Revision 3 to Revision 4
   222  func ThreeToFour(cfg *config.Config) error {
   223  	log.Debugf("migrating from revision 3 to 4")
   224  	ipfsRepoPath, err := maybeRelativizeIPFSPath(cfg)
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	// migrate any existing IPFS repo
   230  	if _, err := os.Stat(ipfsRepoPath); !os.IsNotExist(err) {
   231  		ctx := context.Background()
   232  		if err := qipfs.Migrate(ctx, ipfsRepoPath); err != nil {
   233  			return err
   234  		}
   235  	}
   236  
   237  	if cfg.API != nil {
   238  		cfg.API.Webui = true
   239  	}
   240  	cfg.Automation = config.DefaultAutomation()
   241  	cfg.Revision = 4
   242  
   243  	if err := safeWriteConfig(cfg); err != nil {
   244  		rollbackConfigWrite(cfg)
   245  		return err
   246  	}
   247  
   248  	return nil
   249  }
   250  
   251  func rollbackConfigWrite(cfg *config.Config) {
   252  	cfgPath := cfg.Path()
   253  	if len(cfgPath) == 0 {
   254  		return
   255  	}
   256  	tmpCfgPath := getTmpConfigFilepath(cfgPath)
   257  	if _, err := os.Stat(tmpCfgPath); !os.IsNotExist(err) {
   258  		os.Remove(tmpCfgPath)
   259  	}
   260  }
   261  
   262  func safeWriteConfig(cfg *config.Config) error {
   263  	cfgPath := cfg.Path()
   264  	if len(cfgPath) == 0 {
   265  		return qerr.New(fmt.Errorf("invalid path"), "could not determine config path")
   266  	}
   267  	tmpCfgPath := getTmpConfigFilepath(cfgPath)
   268  	if err := cfg.WriteToFile(tmpCfgPath); err != nil {
   269  		return qerr.New(err, fmt.Sprintf("could not write config to path %s", tmpCfgPath))
   270  	}
   271  	if err := os.Rename(tmpCfgPath, cfgPath); err != nil {
   272  		return qerr.New(err, fmt.Sprintf("could not write config to path %s", cfgPath))
   273  	}
   274  
   275  	return nil
   276  }
   277  
   278  func getTmpConfigFilepath(cfgPath string) string {
   279  	cfgDir := filepath.Dir(cfgPath)
   280  	tmpCfgPath := filepath.Join(cfgDir, "config_updated.yaml")
   281  	return tmpCfgPath
   282  }
   283  
   284  func delIdx(i int, sl []string) []string {
   285  	if i < len(sl)-1 {
   286  		return append(sl[:i], sl[i+1:]...)
   287  	}
   288  
   289  	return sl[:i]
   290  }
   291  
   292  // In qri v0.9.8 & earlier, the IPFS path location was determined by the
   293  // IPFS_PATH env var, and falling back to $HOME/.ipfs.
   294  func configVersionOneIPFSPath() string {
   295  	path := os.Getenv("IPFS_PATH")
   296  	if path != "" {
   297  		return path
   298  	}
   299  	home, err := homedir.Dir()
   300  	if err != nil {
   301  		panic(fmt.Sprintf("Failed to find the home directory: %s", err.Error()))
   302  	}
   303  	return filepath.Join(home, ".ipfs")
   304  }
   305  
   306  func confirm(w io.Writer, r io.Reader, message string) bool {
   307  	input := prompt(w, r, fmt.Sprintf("%s [Y/n]: ", message))
   308  	if input == "" {
   309  		return true
   310  	}
   311  
   312  	return (input == "y" || input == "yes")
   313  }
   314  
   315  func prompt(w io.Writer, r io.Reader, msg string) string {
   316  	var input string
   317  	fmt.Fprintf(w, msg)
   318  	fmt.Fscanln(r, &input)
   319  	return strings.TrimSpace(strings.ToLower(input))
   320  }
   321  
   322  func maybeRelativizeIPFSPath(cfg *config.Config) (string, error) {
   323  	cfgBasePath := filepath.Dir(cfg.Path())
   324  	for i, fsCfg := range cfg.Filesystems {
   325  		if fsCfg.Type == "ipfs" {
   326  			if ipath, ok := fsCfg.Config["path"]; ok {
   327  				if path, ok := ipath.(string); ok {
   328  					if filepath.IsAbs(path) {
   329  						if rel, err := filepath.Rel(cfgBasePath, path); err == nil {
   330  							cfg.Filesystems[i].Config["path"] = rel
   331  							return path, nil
   332  						}
   333  					}
   334  					return filepath.Join(cfgBasePath, path), nil
   335  				}
   336  			}
   337  			return "", fmt.Errorf("IPFS path is not specified")
   338  		}
   339  	}
   340  	return "", fmt.Errorf("no IPFS filesystem is specified")
   341  }
   342  
   343  func maybeRemoveIPFSRepo(cfg *config.Config, oldPath string) error {
   344  	fmt.Println("\nChecking if existing IPFS directory contains non-qri data...")
   345  	repoPath := filepath.Dir(cfg.Path())
   346  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   347  
   348  	r, err := buildrepo.New(ctx, repoPath, cfg)
   349  	if err != nil {
   350  		cancel()
   351  		return err
   352  	}
   353  
   354  	defer gracefulShutdown(cancel, r)
   355  
   356  	// Note: this is intentionally using the new post-migration IPFS repo to judge
   357  	// pin presence, because we can't operate on the old one
   358  	fs := r.Filesystem().Filesystem(qipfs.FilestoreType)
   359  	if fs == nil {
   360  		return nil
   361  	}
   362  
   363  	logbookPaths, err := r.Logbook().AllReferencedDatasetPaths(ctx)
   364  	if err != nil {
   365  		return err
   366  	}
   367  
   368  	paths := map[string]struct{}{
   369  		// add common paths auto-added on IPFS init we can safely ignore
   370  		"/ipld/QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc": {},
   371  		"/ipld/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn": {},
   372  		"/ipld/QmS4ustL54uo8FzR9455qaxZwuMiUhyvMcX9Ba8nUH4uVv": {},
   373  	}
   374  
   375  	for p := range logbookPaths {
   376  		// switch "/ipfs/" prefixes for "/ipld/"
   377  		p = strings.Replace(p, "/ipfs", "/ipld", 1)
   378  		paths[p] = struct{}{}
   379  	}
   380  
   381  	unknownPinCh, err := fs.(*qipfs.Filestore).PinsetDifference(ctx, paths)
   382  	if err != nil {
   383  		return err
   384  	}
   385  
   386  	log.Debugf("checking pins...%#v\n", paths)
   387  
   388  	unknown := []string{}
   389  	for path := range unknownPinCh {
   390  		log.Debugf("checking if unknown pin is a dataset: %s\n", path)
   391  		path = strings.Replace(path, "/ipld", "/ipfs", 1)
   392  		// check if the pinned path is a valid qri dataset, looking for "dataset.json"
   393  		// this check allows us to ignore qri data logbook doesn't know about
   394  		if f, err := fs.Get(ctx, fmt.Sprintf("%s/dataset.json", path)); err == nil {
   395  			f.Close()
   396  		} else {
   397  			unknown = append(unknown, path)
   398  		}
   399  	}
   400  
   401  	if len(unknown) > 0 {
   402  		fmt.Printf(`
   403  Qri left your original IPFS repo in place because it contains pinned data that 
   404  Qri isn't managing. Qri has created an internal copy of your IPFS repo, and no
   405  longer requires the repo at %q
   406  `, oldPath)
   407  		if len(unknown) < 10 {
   408  			fmt.Printf("unknown pins:\n\t%s\n\n", strings.Join(unknown, "\n\t"))
   409  		} else {
   410  			fmt.Printf("\nfound %d unknown pins\n\n", len(unknown))
   411  		}
   412  	} else {
   413  		if err := os.RemoveAll(oldPath); err != nil {
   414  			return err
   415  		}
   416  		fmt.Printf("moved IPFS repo from %q into qri repo\n", oldPath)
   417  	}
   418  
   419  	log.Info("successfully migrated repo, shutting down")
   420  
   421  	return nil
   422  }
   423  
   424  func gracefulShutdown(cancel context.CancelFunc, r repo.Repo) {
   425  	var wg sync.WaitGroup
   426  	go func() {
   427  		<-r.Done()
   428  		wg.Done()
   429  	}()
   430  
   431  	wg.Add(1)
   432  	cancel()
   433  	wg.Wait()
   434  }