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

     1  // Package lib implements core qri business logic. It exports
     2  // canonical methods that a qri instance can perform regardless of
     3  // client interface. API's of any sort must use lib methods
     4  package lib
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	golog "github.com/ipfs/go-log"
    17  	homedir "github.com/mitchellh/go-homedir"
    18  	ma "github.com/multiformats/go-multiaddr"
    19  	manet "github.com/multiformats/go-multiaddr/net"
    20  	"github.com/qri-io/ioes"
    21  	"github.com/qri-io/qfs"
    22  	"github.com/qri-io/qfs/muxfs"
    23  	"github.com/qri-io/qfs/qipfs"
    24  	"github.com/qri-io/qri/auth/key"
    25  	"github.com/qri-io/qri/auth/token"
    26  	"github.com/qri-io/qri/automation"
    27  	"github.com/qri-io/qri/automation/run"
    28  	"github.com/qri-io/qri/automation/trigger"
    29  	"github.com/qri-io/qri/automation/workflow"
    30  	"github.com/qri-io/qri/base"
    31  	"github.com/qri-io/qri/base/dsfs"
    32  	"github.com/qri-io/qri/base/hiddenfile"
    33  	"github.com/qri-io/qri/collection"
    34  	"github.com/qri-io/qri/config"
    35  	"github.com/qri-io/qri/config/migrate"
    36  	"github.com/qri-io/qri/dscache"
    37  	"github.com/qri-io/qri/dsref"
    38  	qrierr "github.com/qri-io/qri/errors"
    39  	"github.com/qri-io/qri/event"
    40  	qhttp "github.com/qri-io/qri/lib/http"
    41  	"github.com/qri-io/qri/logbook"
    42  	"github.com/qri-io/qri/p2p"
    43  	"github.com/qri-io/qri/profile"
    44  	"github.com/qri-io/qri/registry/regclient"
    45  	"github.com/qri-io/qri/remote"
    46  	"github.com/qri-io/qri/repo"
    47  	"github.com/qri-io/qri/repo/buildrepo"
    48  	"github.com/qri-io/qri/stats"
    49  )
    50  
    51  var (
    52  	// ErrBadArgs is an error for when a user provides bad arguments
    53  	ErrBadArgs = errors.New("bad arguments provided")
    54  	// ErrNoRepo is an error for  when a repo does not exist at a given path
    55  	ErrNoRepo = errors.New("no repo exists")
    56  
    57  	log = golog.Logger("lib")
    58  )
    59  
    60  // Methods is a related set of library functions
    61  type Methods interface {
    62  	// CoreRequestsName confirms participation in the CoreRequests interface while
    63  	// also giving a human readable string for logging purposes
    64  	// TODO (b5): rename this interface to "MethodsName", or remove entirely
    65  	CoreRequestsName() string
    66  }
    67  
    68  // InstanceOptions provides details to NewInstance.
    69  // New will alter InstanceOptions by applying
    70  // any provided Option functions
    71  // to distinguish "Options" from "Config":
    72  // * Options contains state that can only be determined at runtime
    73  // * Config consists only of static values stored in a configuration file
    74  // Options may override config in specific cases to avoid undefined state
    75  type InstanceOptions struct {
    76  	Cfg     *config.Config
    77  	Streams ioes.IOStreams
    78  
    79  	statsCache              stats.Cache
    80  	node                    *p2p.QriNode
    81  	repo                    repo.Repo
    82  	qfs                     *muxfs.Mux
    83  	dscache                 *dscache.Dscache
    84  	regclient               *regclient.Client
    85  	remoteClientConstructor remote.ClientConstructor
    86  	logbook                 *logbook.Book
    87  	keyStore                key.Store
    88  	profiles                profile.Store
    89  	bus                     event.Bus
    90  	collectionSet           collection.Set
    91  	tokenProvider           token.Provider
    92  	logAll                  bool
    93  	automationOptions       *automation.OrchestratorOptions
    94  
    95  	remoteMockClient bool
    96  	// use OptRemoteOptions to set this
    97  	remoteOptsFuncs []remote.OptionsFunc
    98  
    99  	eventHandler event.Handler
   100  	events       []event.Type
   101  }
   102  
   103  // Option is a function that manipulates config details when fed to New(). Fields on
   104  // the o parameter may be null, functions cannot assume the Config is non-null.
   105  type Option func(o *InstanceOptions) error
   106  
   107  // OptConfig supplies a configuration directly
   108  func OptConfig(cfg *config.Config) Option {
   109  	return func(o *InstanceOptions) error {
   110  		o.Cfg = cfg
   111  		return nil
   112  	}
   113  }
   114  
   115  // OptSetIPFSPath sets the directory to read IPFS from.
   116  // Passing the empty string adjusts qri to use the go-ipfs default:
   117  // first checking the IPFS_PATH env variable, then falling back to $HOME/.ipfs
   118  // if no ipfs filesystem is configured, this option creates one
   119  func OptSetIPFSPath(path string) Option {
   120  	return func(o *InstanceOptions) error {
   121  		if o.Cfg == nil {
   122  			return fmt.Errorf("config is nil, can't set IPFS path")
   123  		}
   124  		if o.Cfg.Filesystems == nil {
   125  			return fmt.Errorf("config Filesystems field is nil, can't set IPFS path")
   126  		}
   127  
   128  		if path == "" {
   129  			path = os.Getenv("IPFS_PATH")
   130  			if path == "" {
   131  				dir, err := homedir.Dir()
   132  				if err != nil {
   133  					return err
   134  				}
   135  				path = filepath.Join(dir, ".ipfs")
   136  			}
   137  		}
   138  
   139  		for i, fsc := range o.Cfg.Filesystems {
   140  			if fsc.Type == "ipfs" {
   141  				fsConfig := o.Cfg.Filesystems[i]
   142  				if fsConfig.Config == nil {
   143  					fsConfig.Config = map[string]interface{}{}
   144  				}
   145  				fsConfig.Config["path"] = path
   146  				return nil
   147  			}
   148  		}
   149  
   150  		o.Cfg.Filesystems = append([]qfs.Config{
   151  			{
   152  				Type: "ipfs",
   153  				Config: map[string]interface{}{
   154  					"path": path,
   155  				},
   156  			},
   157  		}, o.Cfg.Filesystems...)
   158  
   159  		return nil
   160  	}
   161  }
   162  
   163  // OptIOStreams sets the input IOStreams
   164  func OptIOStreams(streams ioes.IOStreams) Option {
   165  	return func(o *InstanceOptions) error {
   166  		o.Streams = streams
   167  		return nil
   168  	}
   169  }
   170  
   171  // OptStdIOStreams sets treams to std, stdout, & stderr
   172  func OptStdIOStreams() Option {
   173  	return func(o *InstanceOptions) error {
   174  		o.Streams = ioes.NewStdIOStreams()
   175  		return nil
   176  	}
   177  }
   178  
   179  // OptSetOpenFileTimeout sets a timeout duration for opening files
   180  func OptSetOpenFileTimeout(d time.Duration) Option {
   181  	return func(_ *InstanceOptions) error {
   182  		dsfs.OpenFileTimeoutDuration = d
   183  		return nil
   184  	}
   185  }
   186  
   187  // OptCheckConfigMigrations checks for any configuration migrations that may
   188  // need to be run. running & updating config if so
   189  func OptCheckConfigMigrations(shouldRunFn func() bool, errOnSuccess bool) Option {
   190  	return func(o *InstanceOptions) error {
   191  		if o.Cfg == nil {
   192  			return fmt.Errorf("no config file to check for migrations")
   193  		}
   194  
   195  		err := migrate.RunMigrations(o.Streams, o.Cfg, shouldRunFn, errOnSuccess)
   196  		if err != nil {
   197  			return err
   198  		}
   199  
   200  		return nil
   201  	}
   202  }
   203  
   204  // OptNoBootstrap ensures the node will not attempt to bootstrap to any other nodes
   205  // in the network
   206  func OptNoBootstrap() Option {
   207  	return func(o *InstanceOptions) error {
   208  		// ensure qri p2p bootstrap addresses are empty
   209  		o.Cfg.P2P.BootstrapAddrs = []string{}
   210  		// if we have a qipfs config, pass the `disableBootstrap` flag
   211  		for _, qfsCfg := range o.Cfg.Filesystems {
   212  			if qfsCfg.Type == qipfs.FilestoreType {
   213  				qfsCfg.Config["disableBootstrap"] = true
   214  			}
   215  		}
   216  		return nil
   217  	}
   218  }
   219  
   220  // OptSetLogAll sets the logAll value so that debug level logging is enabled for all qri packages
   221  func OptSetLogAll(logAll bool) Option {
   222  	return func(o *InstanceOptions) error {
   223  		o.logAll = logAll
   224  		return nil
   225  	}
   226  }
   227  
   228  // OptRemoteClientConstructor provides a constructor function for creating a
   229  // remote client, which will be used when creating the instance. Use this to
   230  // override the remoteClient implementation used by instance
   231  func OptRemoteClientConstructor(c remote.ClientConstructor) Option {
   232  	return func(o *InstanceOptions) error {
   233  		o.remoteClientConstructor = c
   234  		return nil
   235  	}
   236  }
   237  
   238  // OptRemoteServerOptions provides options to the remote server the provided
   239  // function is called with the Qri configuration-derived remote settings applied
   240  // allowing partial-overrides.
   241  func OptRemoteServerOptions(fns []remote.OptionsFunc) Option {
   242  	return func(o *InstanceOptions) error {
   243  		o.remoteOptsFuncs = fns
   244  		return nil
   245  	}
   246  }
   247  
   248  // OptQriNode configures bring-your-own qri node
   249  func OptQriNode(node *p2p.QriNode) Option {
   250  	return func(o *InstanceOptions) error {
   251  		o.node = node
   252  		if o.node.Repo != nil && o.repo == nil {
   253  			o.repo = o.node.Repo
   254  		}
   255  		if o.node.Repo.Filesystem() != nil {
   256  			o.qfs = o.node.Repo.Filesystem()
   257  		}
   258  		return nil
   259  	}
   260  }
   261  
   262  // OptRegistryClient overrides any configured registry client
   263  func OptRegistryClient(cli *regclient.Client) Option {
   264  	return func(o *InstanceOptions) error {
   265  		o.regclient = cli
   266  		return nil
   267  	}
   268  }
   269  
   270  // OptStatsCache overrides the configured stats cache
   271  func OptStatsCache(statsCache stats.Cache) Option {
   272  	return func(o *InstanceOptions) error {
   273  		o.statsCache = statsCache
   274  		return nil
   275  	}
   276  }
   277  
   278  // OptCollectionSet provides a collection implementation
   279  func OptCollectionSet(c collection.Set) Option {
   280  	return func(o *InstanceOptions) error {
   281  		o.collectionSet = c
   282  		return nil
   283  	}
   284  }
   285  
   286  // OptTokenProvider provides a token provider implementation
   287  func OptTokenProvider(t token.Provider) Option {
   288  	return func(o *InstanceOptions) error {
   289  		o.tokenProvider = t
   290  		return nil
   291  	}
   292  }
   293  
   294  // OptOrchestratorOptions provides orchestrator options for the creation of an Orchestrator
   295  func OptOrchestratorOptions(a *automation.OrchestratorOptions) Option {
   296  	return func(o *InstanceOptions) error {
   297  		o.automationOptions = a
   298  		return nil
   299  	}
   300  }
   301  
   302  // OptLogbook overrides the configured logbook with a manually provided one
   303  func OptLogbook(bk *logbook.Book) Option {
   304  	return func(o *InstanceOptions) error {
   305  		o.logbook = bk
   306  		return nil
   307  	}
   308  }
   309  
   310  // OptEventHandler provides an event handler & list of event types to subscribe
   311  // to. The canonical list of events a qri instance emits are defined in the
   312  // github.com/qri-io/qri/event package
   313  // plase note that event handlers in qri are synchronous. A handler function
   314  // that takes a long time to return will slow down the performance of qri
   315  // generally
   316  func OptEventHandler(handler event.Handler, events ...event.Type) Option {
   317  	return func(o *InstanceOptions) error {
   318  		o.eventHandler = handler
   319  		o.events = events
   320  		return nil
   321  	}
   322  }
   323  
   324  // OptProfiles supplies a profile store for the instance
   325  func OptProfiles(pros profile.Store) Option {
   326  	return func(o *InstanceOptions) error {
   327  		o.profiles = pros
   328  		return nil
   329  	}
   330  }
   331  
   332  // OptKeyStore supplies a key store for the instance
   333  func OptKeyStore(keys key.Store) Option {
   334  	return func(o *InstanceOptions) error {
   335  		o.keyStore = keys
   336  		return nil
   337  	}
   338  }
   339  
   340  // OptBus overrides the configured `event.Bus` with a manually provided one
   341  func OptBus(bus event.Bus) Option {
   342  	return func(o *InstanceOptions) error {
   343  		o.bus = bus
   344  		return nil
   345  	}
   346  }
   347  
   348  // OptDscache overrides the configured `dscache.Dscache` with a manually provided one
   349  func OptDscache(dscache *dscache.Dscache) Option {
   350  	return func(o *InstanceOptions) error {
   351  		o.dscache = dscache
   352  		return nil
   353  	}
   354  }
   355  
   356  // NewInstance creates a new Qri Instance, if no Option funcs are provided,
   357  // New uses a default set of Option funcs. Any Option functions passed to this
   358  // function must check whether their fields are nil or not.
   359  func NewInstance(ctx context.Context, repoPath string, opts ...Option) (qri *Instance, err error) {
   360  	log.Debugw("NewInstance", "repoPath", repoPath, "opts", opts)
   361  	ctx, cancel := context.WithCancel(ctx)
   362  	ok := false
   363  	defer func() {
   364  		if !ok {
   365  			cancel()
   366  		}
   367  	}()
   368  
   369  	go func() {
   370  		select {
   371  		case <-ctx.Done():
   372  			log.Debug("Instance context canceled")
   373  			return
   374  		}
   375  	}()
   376  
   377  	if repoPath == "" {
   378  		return nil, fmt.Errorf("repo path is required")
   379  	}
   380  
   381  	o := &InstanceOptions{}
   382  
   383  	// attempt to load a base configuration from repoPath
   384  	needsMigration := false
   385  	if o.Cfg, err = loadRepoConfig(repoPath); err != nil {
   386  		log.Debugf("loading config: %s", err)
   387  		if o.Cfg != nil && o.Cfg.Revision != config.CurrentConfigRevision {
   388  			log.Debugf("config requires a migration from revision %d to %d", o.Cfg.Revision, config.CurrentConfigRevision)
   389  			needsMigration = true
   390  		}
   391  		if !needsMigration {
   392  			return
   393  		}
   394  	}
   395  
   396  	if len(opts) == 0 {
   397  		// default to a standard composition of Option funcs
   398  		opts = []Option{
   399  			OptStdIOStreams(),
   400  			OptCheckConfigMigrations(func() bool { return true }, false),
   401  		}
   402  	}
   403  	for _, opt := range opts {
   404  		if err = opt(o); err != nil {
   405  			return nil, err
   406  		}
   407  	}
   408  
   409  	if needsMigration {
   410  		if o.Cfg, err = loadRepoConfig(repoPath); err != nil {
   411  			log.Error("loading config: %s", err)
   412  			return
   413  		}
   414  	}
   415  
   416  	cfg := o.Cfg
   417  	if cfg == nil {
   418  		// If at this point we don't have a configuration pointer one couldn't be
   419  		// loaded from repoPath, and a configuration wasn't provided through Options,
   420  		// so qri needs to be set up
   421  		err = fmt.Errorf("no qri repo found, please run `qri setup`")
   422  		return
   423  	} else if err = cfg.Validate(); err != nil {
   424  		return
   425  	}
   426  
   427  	// If configuration does not have a path assigned, but the repo has a path and
   428  	// is stored on the filesystem, add that path to the configuration.
   429  	if cfg.Repo.Type == "fs" && cfg.Path() == "" {
   430  		cfg.SetPath(filepath.Join(repoPath, "config.yaml"))
   431  	}
   432  
   433  	inst := &Instance{
   434  		cancel: cancel,
   435  		doneCh: make(chan struct{}),
   436  
   437  		repoPath: repoPath,
   438  		cfg:      cfg,
   439  
   440  		qfs:           o.qfs,
   441  		repo:          o.repo,
   442  		node:          o.node,
   443  		streams:       o.Streams,
   444  		registry:      o.regclient,
   445  		logbook:       o.logbook,
   446  		keystore:      o.keyStore,
   447  		tokenProvider: o.tokenProvider,
   448  		dscache:       o.dscache,
   449  		profiles:      o.profiles,
   450  		bus:           o.bus,
   451  		appCtx:        ctx,
   452  	}
   453  	qri = inst
   454  
   455  	// configure logging straight away
   456  	if cfg != nil && cfg.Logging != nil {
   457  		for name, level := range cfg.Logging.Levels {
   458  			golog.SetLogLevel(name, level)
   459  		}
   460  	}
   461  
   462  	// if logAll is enabled, turn on debug level logging for all qri packages. Packages need to
   463  	// be explicitly enumerated here
   464  	if o.logAll {
   465  		allPackages := []string{"automation", "qriapi", "qrip2p", "base", "changes", "cmd", "config", "dsref", "dsfs", "friendly", "lib", "logbook", "profile", "repo", "registry", "startf", "sql", "token"}
   466  		for _, name := range allPackages {
   467  			golog.SetLogLevel(name, "debug")
   468  		}
   469  		log.Debugf("--log-all set: turning on logging for all activity")
   470  	}
   471  
   472  	inst.RegisterMethods()
   473  
   474  	if cfg.API != nil && cfg.API.Enabled {
   475  		// check if we're operating over RPC by dialing API.Address to check for a connection
   476  		addr, err := ma.NewMultiaddr(cfg.API.Address)
   477  		if err != nil {
   478  			return nil, qrierr.New(err, fmt.Sprintf("invalid config.api.address value: %q", cfg.API.Address))
   479  		}
   480  		if _, dialErr := manet.Dial(addr); dialErr == nil {
   481  			// we have a connection
   482  			inst.http, err = qhttp.NewClient(cfg.API.Address)
   483  			if err != nil {
   484  				return nil, err
   485  			}
   486  
   487  			go inst.waitForAllDone()
   488  			return qri, err
   489  		}
   490  	}
   491  
   492  	if inst.bus == nil {
   493  		inst.bus = newEventBus(ctx)
   494  	}
   495  
   496  	if o.eventHandler != nil && o.events != nil {
   497  		inst.bus.SubscribeTypes(o.eventHandler, o.events...)
   498  	}
   499  
   500  	if inst.qfs == nil {
   501  		inst.qfs, err = buildrepo.NewFilesystem(ctx, cfg)
   502  		if err != nil {
   503  			return nil, err
   504  		}
   505  
   506  		go func() {
   507  			inst.releasers.Add(1)
   508  			<-inst.qfs.Done()
   509  			inst.doneErr = inst.qfs.DoneErr()
   510  			inst.releasers.Done()
   511  		}()
   512  	}
   513  
   514  	if inst.keystore == nil {
   515  		inst.keystore, err = key.NewStore(cfg)
   516  		if err != nil {
   517  			log.Debugw("initializing keystore", "err", err)
   518  			return nil, err
   519  		}
   520  	}
   521  
   522  	if inst.profiles == nil {
   523  		if inst.profiles, err = profile.NewStore(ctx, cfg, inst.keystore); err != nil {
   524  			return nil, fmt.Errorf("initializing profile service: %w", err)
   525  		}
   526  	}
   527  
   528  	if inst.tokenProvider == nil {
   529  		if inst.tokenProvider, err = token.NewProvider(inst.profiles, inst.keystore); err != nil {
   530  			return nil, fmt.Errorf("initializing token provider: %w", err)
   531  		}
   532  	}
   533  
   534  	pro := inst.profiles.Owner(ctx)
   535  
   536  	if inst.logbook == nil {
   537  		inst.logbook, err = newLogbook(inst.qfs, cfg, inst.bus, pro, inst.repoPath)
   538  		if err != nil {
   539  			return nil, fmt.Errorf("intializing logbook: %w", err)
   540  		}
   541  	}
   542  
   543  	if inst.registry == nil {
   544  		inst.registry = newRegClient(ctx, cfg)
   545  	}
   546  
   547  	if inst.dscache == nil {
   548  		inst.dscache, err = newDscache(ctx, inst.qfs, inst.bus, pro.Peername, inst.repoPath)
   549  		if err != nil {
   550  			log.Error("initalizing dscache:", err.Error())
   551  			return nil, fmt.Errorf("newDsache: %w", err)
   552  		}
   553  	}
   554  
   555  	if inst.repo == nil {
   556  		if inst.repo, err = buildrepo.New(ctx, inst.repoPath, cfg, func(o *buildrepo.Options) {
   557  			o.Bus = inst.bus
   558  			o.Filesystem = inst.qfs
   559  			o.Profiles = inst.profiles
   560  			o.Logbook = inst.logbook
   561  			o.Dscache = inst.dscache
   562  			o.Keystore = inst.keystore
   563  		}); err != nil {
   564  			log.Error("intializing repo:", err.Error())
   565  			return nil, fmt.Errorf("newRepo: %w", err)
   566  		}
   567  	}
   568  
   569  	if inst.compStat == nil {
   570  		inst.compStat = base.NewComponentStatus(ctx, inst.qfs)
   571  	}
   572  
   573  	// Try to make the repo a hidden directory, but it's okay if we can't. Ignore the error.
   574  	_ = hiddenfile.SetFileHidden(inst.repoPath)
   575  
   576  	if o.statsCache != nil {
   577  		inst.stats = stats.New(o.statsCache)
   578  	} else if inst.stats == nil {
   579  		if inst.stats, err = newStats(cfg, inst.repoPath); err != nil {
   580  			return nil, err
   581  		}
   582  	}
   583  
   584  	if inst.node == nil {
   585  		var localResolver dsref.Resolver
   586  		localResolver, err = inst.resolverForSource("local")
   587  		if err != nil {
   588  			return
   589  		}
   590  		if inst.node, err = p2p.NewQriNode(inst.repo, cfg.P2P, inst.bus, localResolver); err != nil {
   591  			log.Error("intializing p2p:", err.Error())
   592  			return
   593  		}
   594  	}
   595  
   596  	if inst.node != nil {
   597  		inst.node.LocalStreams = inst.streams
   598  
   599  		newClient := remote.NewClient
   600  		if o.remoteClientConstructor != nil {
   601  			newClient = o.remoteClientConstructor
   602  		}
   603  
   604  		if inst.remoteClient, err = newClient(ctx, inst.node, inst.bus); err != nil {
   605  			return nil, err
   606  		}
   607  
   608  		go func() {
   609  			inst.releasers.Add(1)
   610  			<-inst.remoteClient.Done()
   611  			inst.releasers.Done()
   612  		}()
   613  
   614  		if cfg.RemoteServer != nil && cfg.RemoteServer.Enabled {
   615  			if o.remoteOptsFuncs == nil {
   616  				o.remoteOptsFuncs = []remote.OptionsFunc{}
   617  			}
   618  
   619  			localResolver, resolverErr := inst.resolverForSource("local")
   620  			if resolverErr != nil {
   621  				return nil, resolverErr
   622  			}
   623  
   624  			if inst.remoteServer, err = remote.NewServer(inst.node, cfg.RemoteServer, localResolver, inst.bus, o.remoteOptsFuncs...); err != nil {
   625  				log.Error("intializing remote:", err.Error())
   626  				return
   627  			}
   628  			// TODO (ramfox): we need to preserve these options
   629  			// for if we need to re initalize the remote & don't have access
   630  			// to those options again (this happens in the `GoOnline` func below)
   631  			inst.remoteOptsFuncs = o.remoteOptsFuncs
   632  		}
   633  	}
   634  
   635  	if o.collectionSet == nil && inst.repo != nil {
   636  		set, err := collection.NewLocalSet(ctx, repoPath, func(o *collection.LocalSetOptions) {
   637  			o.MigrateRepo = inst.repo
   638  		})
   639  		if err != nil {
   640  			return nil, err
   641  		}
   642  		o.collectionSet = set
   643  	}
   644  
   645  	if o.collectionSet != nil {
   646  		inst.collections, err = collection.NewSetMaintainer(ctx, inst.bus, o.collectionSet)
   647  		if err != nil {
   648  			return nil, err
   649  		}
   650  	}
   651  
   652  	if o.automationOptions == nil {
   653  		// TODO(ramfox): using `DefaultOrchestratorOptions` func for now to generate
   654  		// basic orchestrator options. When we get the automation configuration settled
   655  		// we will build a more robust solution
   656  		orchestratorOpts, err := automation.DefaultOrchestratorOptions(inst.bus, inst.repoPath)
   657  		if err != nil {
   658  			return nil, err
   659  		}
   660  		o.automationOptions = &orchestratorOpts
   661  	}
   662  	inst.automation, err = automation.NewOrchestrator(ctx, inst.bus, &runner{owner: inst}, *o.automationOptions)
   663  	if err != nil {
   664  		return nil, err
   665  	}
   666  
   667  	go inst.waitForAllDone()
   668  	go func() {
   669  		if err := inst.bus.Publish(ctx, event.ETInstanceConstructed, nil); err != nil {
   670  			log.Debugf("instance construction: %w", err)
   671  			err = nil
   672  		}
   673  	}()
   674  
   675  	ok = true
   676  	return
   677  }
   678  
   679  // TODO (b5): this is a repo layout assertion, move to repo package?
   680  func loadRepoConfig(repoPath string) (*config.Config, error) {
   681  	path := filepath.Join(repoPath, "config.yaml")
   682  
   683  	if _, e := os.Stat(path); os.IsNotExist(e) {
   684  		return nil, nil
   685  	}
   686  
   687  	return config.ReadFromFile(path)
   688  }
   689  
   690  func newRegClient(ctx context.Context, cfg *config.Config) (rc *regclient.Client) {
   691  	if cfg.Registry != nil {
   692  		switch cfg.Registry.Location {
   693  		case "":
   694  			return rc
   695  		default:
   696  			return regclient.NewClient(&regclient.Config{
   697  				Location: cfg.Registry.Location,
   698  			})
   699  		}
   700  	}
   701  
   702  	return nil
   703  }
   704  
   705  func newLogbook(fs qfs.Filesystem, cfg *config.Config, bus event.Bus, pro *profile.Profile, repoPath string) (book *logbook.Book, err error) {
   706  	logbookPath := filepath.Join(repoPath, "logbook.qfb")
   707  	return logbook.NewJournal(*pro, bus, fs, logbookPath)
   708  }
   709  
   710  func newDscache(ctx context.Context, fs qfs.Filesystem, bus event.Bus, username, repoPath string) (*dscache.Dscache, error) {
   711  	dscachePath := filepath.Join(repoPath, "dscache.qfb")
   712  	return dscache.NewDscache(ctx, fs, bus, username, dscachePath), nil
   713  }
   714  
   715  func newEventBus(ctx context.Context) event.Bus {
   716  	return event.NewBus(ctx)
   717  }
   718  
   719  func newStats(cfg *config.Config, repoPath string) (*stats.Service, error) {
   720  	// The stats cache default location is repoPath/stats
   721  	// can be overridden in the config: cfg.Stats.Path
   722  	path := filepath.Join(repoPath, "stats")
   723  	if cfg.Stats == nil {
   724  		return stats.New(nil), nil
   725  	}
   726  	if cfg.Stats.Cache.Path != "" {
   727  		path = cfg.Stats.Cache.Path
   728  	}
   729  	switch cfg.Stats.Cache.Type {
   730  	case "fs", "local":
   731  		cache, err := stats.NewLocalCache(path, int64(cfg.Stats.Cache.MaxSize))
   732  		if err != nil {
   733  			return nil, err
   734  		}
   735  		return stats.New(cache), nil
   736  	default:
   737  		return stats.New(nil), nil
   738  	}
   739  }
   740  
   741  // NewInstanceFromConfigAndNode is a temporary solution to create an instance from an
   742  // already-allocated QriNode & configuration
   743  // don't write new code that relies on this, instead create a configuration
   744  // and options that can be fed to NewInstance
   745  // This function must only be used for testing purposes
   746  // TODO(ramfox): remove and replace call sites with `NewInstance`
   747  func NewInstanceFromConfigAndNode(ctx context.Context, cfg *config.Config, node *p2p.QriNode) *Instance {
   748  	return NewInstanceFromConfigAndNodeAndBus(ctx, cfg, node, event.NilBus)
   749  }
   750  
   751  // NewInstanceFromConfigAndNodeAndBus adds a bus argument to the horrible, hacky
   752  // instance construtor
   753  // TODO(ramfox): remove and replace call sites with `NewInstance`
   754  func NewInstanceFromConfigAndNodeAndBus(ctx context.Context, cfg *config.Config, node *p2p.QriNode, bus event.Bus) *Instance {
   755  	return NewInstanceFromConfigAndNodeAndBusAndOrchestratorOpts(ctx, cfg, node, bus, nil)
   756  }
   757  
   758  // NewInstanceFromConfigAndNodeAndBusAndOrchestratorOpts adds orchestrator opts to the
   759  // horrible, hacky instance constructor
   760  // This function must only be used for testing purpose
   761  // TODO(ramfox): remove and replace call sites with `NewInstance`
   762  func NewInstanceFromConfigAndNodeAndBusAndOrchestratorOpts(ctx context.Context, cfg *config.Config, node *p2p.QriNode, bus event.Bus, o *automation.OrchestratorOptions) *Instance {
   763  	ctx, cancel := context.WithCancel(ctx)
   764  
   765  	r := node.Repo
   766  	pro := r.Profiles().Owner(ctx)
   767  	dc := dscache.NewDscache(ctx, r.Filesystem(), bus, pro.Peername, "")
   768  
   769  	// TODO (b5) - lots of tests pass "DefaultConfigForTesting", which uses a different peername /
   770  	// identity from what the repo already has. This disagreement is a potential source of bugs
   771  	// we should fix this by getting over to lib.NewInstance ASAP
   772  	if cfg.Profile.Peername != pro.Peername {
   773  		cfg.Profile.Peername = pro.Peername
   774  	}
   775  
   776  	inst := &Instance{
   777  		cancel: cancel,
   778  		doneCh: make(chan struct{}),
   779  
   780  		cfg:      cfg,
   781  		node:     node,
   782  		dscache:  dc,
   783  		logbook:  r.Logbook(),
   784  		profiles: r.Profiles(),
   785  		appCtx:   ctx,
   786  	}
   787  	inst.RegisterMethods()
   788  
   789  	inst.stats = stats.New(nil)
   790  
   791  	if node != nil && r != nil {
   792  		inst.repo = r
   793  		inst.bus = bus
   794  		inst.qfs = r.Filesystem()
   795  	}
   796  
   797  	var err error
   798  	// TODO(ramfox): using `DefaultOrchestratorOptions` func for now to generate
   799  	// basic orchestrator options. When we get the automation configuration settled
   800  	// we will build a more robust solution
   801  	if o == nil {
   802  		o = &automation.OrchestratorOptions{
   803  			WorkflowStore: workflow.NewMemStore(),
   804  			Listeners: []trigger.Listener{
   805  				trigger.NewRuntimeListener(ctx, inst.bus),
   806  			},
   807  			RunStore: run.NewMemStore(),
   808  		}
   809  	}
   810  	inst.automation, err = automation.NewOrchestrator(ctx, inst.bus, &runner{owner: inst}, *o)
   811  	if err != nil {
   812  		cancel()
   813  		panic(err)
   814  	}
   815  
   816  	inst.remoteClient, err = remote.NewClient(ctx, node, inst.bus)
   817  	if err != nil {
   818  		cancel()
   819  		panic(err)
   820  	}
   821  
   822  	set, err := collection.NewLocalSet(ctx, "", func(o *collection.LocalSetOptions) {
   823  		o.MigrateRepo = inst.repo
   824  	})
   825  	if err != nil {
   826  		cancel()
   827  		panic(err)
   828  	}
   829  
   830  	inst.collections, err = collection.NewSetMaintainer(ctx, inst.bus, set)
   831  	if err != nil {
   832  		cancel()
   833  		panic(err)
   834  	}
   835  
   836  	inst.releasers.Add(1)
   837  	go func() {
   838  		<-inst.remoteClient.Done()
   839  		inst.releasers.Done()
   840  	}()
   841  
   842  	go inst.waitForAllDone()
   843  	return inst
   844  }
   845  
   846  // Instance bundles the foundational values qri relies on, including a qri
   847  // configuration, p2p node, and base context.
   848  // An instance wraps required state for for "Method" constructors, which
   849  // contain qri business logic. Think of instance as the "core" of the qri
   850  // ecosystem. Create an Instance pointer with NewInstance
   851  type Instance struct {
   852  	repoPath string
   853  	cfg      *config.Config
   854  
   855  	regMethods *regMethodSet
   856  
   857  	streams       ioes.IOStreams
   858  	repo          repo.Repo
   859  	node          *p2p.QriNode
   860  	qfs           *muxfs.Mux
   861  	remoteServer  *remote.Server
   862  	remoteClient  remote.Client
   863  	registry      *regclient.Client
   864  	stats         *stats.Service
   865  	logbook       *logbook.Book
   866  	dscache       *dscache.Dscache
   867  	collections   *collection.SetMaintainer
   868  	automation    *automation.Orchestrator
   869  	compStat      *base.ComponentStatus
   870  	tokenProvider token.Provider
   871  	bus           event.Bus
   872  	appCtx        context.Context
   873  
   874  	profiles profile.Store
   875  	keystore key.Store
   876  
   877  	remoteOptsFuncs []remote.OptionsFunc
   878  
   879  	http *qhttp.Client
   880  
   881  	cancel    context.CancelFunc
   882  	doneCh    chan struct{}
   883  	doneErr   error
   884  	releasers sync.WaitGroup
   885  }
   886  
   887  // ErrP2PDisabled error indicates p2p connectivity is disabled by configuration
   888  var ErrP2PDisabled = fmt.Errorf("peer-2-peer networking is disabled")
   889  
   890  // ConnectP2P connects an instance's peer-2-peer node
   891  func (inst *Instance) ConnectP2P(ctx context.Context) (err error) {
   892  	if inst.cfg.P2P == nil || !inst.cfg.P2P.Enabled {
   893  		return ErrP2PDisabled
   894  	}
   895  
   896  	if err = inst.node.GoOnline(ctx); err != nil {
   897  		log.Debugw("connecting instance p2p node", "err", err.Error())
   898  		return
   899  	}
   900  
   901  	// for now if we have an IPFS node instance, node.GoOnline has to make a new
   902  	// instance to connect properly. If remoteClient or remote retains the reference to the
   903  	// old instance, we run into issues where the online instance can't "see"
   904  	// the additions. We fix that by shutting down the previous client and
   905  	// re-initializing the client and remote with the new instance
   906  	if inst.remoteClient != nil {
   907  		<-inst.remoteClient.Shutdown()
   908  	}
   909  	// NOTE: the previous remote client got its context from the context that is
   910  	// tied to the life of the instance. This one is tied to the life of the
   911  	// `Connect` function. The instance is responsible for cleaning up the
   912  	// remoteClient, since it cannot rely on this context to cancel at the same
   913  	// time as the context of the instance does
   914  	if inst.remoteClient, err = remote.NewClient(ctx, inst.node, inst.bus); err != nil {
   915  		log.Debugf("remote.NewClient error=%q", err)
   916  		return
   917  	}
   918  	go func() {
   919  		inst.releasers.Add(1)
   920  		<-inst.remoteClient.Done()
   921  		inst.releasers.Done()
   922  	}()
   923  
   924  	if inst.cfg.RemoteServer != nil && inst.cfg.RemoteServer.Enabled {
   925  		localResolver, err := inst.resolverForSource("local")
   926  		if err != nil {
   927  			return err
   928  		}
   929  		if inst.remoteServer, err = remote.NewServer(inst.node, inst.cfg.RemoteServer, localResolver, inst.bus, inst.remoteOptsFuncs...); err != nil {
   930  			log.Debugw("remote.NewServer", "err", err)
   931  			return err
   932  		}
   933  		if err = inst.remoteServer.GoOnline(ctx); err != nil {
   934  			log.Debugw("remote.GoOnline", "err", err)
   935  			return err
   936  		}
   937  	}
   938  
   939  	return nil
   940  }
   941  
   942  // ErrAutomationDisabled error indicates automation is disabled by configuration
   943  var ErrAutomationDisabled = fmt.Errorf("automation is disabled")
   944  
   945  // AutomationListen starts the automation orchestrator listening for automation
   946  // trigger
   947  func (inst *Instance) AutomationListen(ctx context.Context) (err error) {
   948  	if inst.cfg.Automation != nil && !inst.cfg.Automation.Enabled {
   949  		return ErrAutomationDisabled
   950  	}
   951  
   952  	err = inst.automation.Start(ctx)
   953  	if err != nil {
   954  		return
   955  	}
   956  	go func() {
   957  		inst.releasers.Add(1)
   958  		<-inst.automation.Done()
   959  		inst.releasers.Done()
   960  	}()
   961  
   962  	return nil
   963  }
   964  
   965  // Access returns the AccessMethods that Instance has registered
   966  func (inst *Instance) Access() AccessMethods {
   967  	return AccessMethods{d: inst}
   968  }
   969  
   970  // Automation returns the AutomationMethods that Instance has registered
   971  func (inst *Instance) Automation() AutomationMethods {
   972  	return AutomationMethods{d: inst}
   973  }
   974  
   975  // Collection returns the CollectionMethods that Instance has registered
   976  func (inst *Instance) Collection() CollectionMethods {
   977  	return CollectionMethods{d: inst}
   978  }
   979  
   980  // Config returns the ConfigMethods that Instance has registered
   981  func (inst *Instance) Config() ConfigMethods {
   982  	return ConfigMethods{d: inst}
   983  }
   984  
   985  // Dataset returns the DatasetMethods that Instance has registered
   986  func (inst *Instance) Dataset() DatasetMethods {
   987  	return DatasetMethods{d: inst}
   988  }
   989  
   990  // Diff returns the DiffMethods that Instance has registered
   991  func (inst *Instance) Diff() DiffMethods {
   992  	return DiffMethods{d: inst}
   993  }
   994  
   995  // Log returns the LogMethods that Instance has registered
   996  func (inst *Instance) Log() LogMethods {
   997  	return LogMethods{d: inst}
   998  }
   999  
  1000  // Peer returns the PeerMethods that Instance has registered
  1001  func (inst *Instance) Peer() PeerMethods {
  1002  	return PeerMethods{d: inst}
  1003  }
  1004  
  1005  // Profile returns the ProfileMethods that Instance has registered
  1006  func (inst *Instance) Profile() ProfileMethods {
  1007  	return ProfileMethods{d: inst}
  1008  }
  1009  
  1010  // Registry returns the RegistryMethods that Instance has registered
  1011  func (inst *Instance) Registry() RegistryClientMethods {
  1012  	return RegistryClientMethods{d: inst}
  1013  }
  1014  
  1015  // Follow returns the FollowMethods that Instance has registered
  1016  func (inst *Instance) Follow() FollowMethods {
  1017  	return FollowMethods{d: inst}
  1018  }
  1019  
  1020  // Remote returns the RemoteMethods that Instance has registered
  1021  func (inst *Instance) Remote() RemoteMethods {
  1022  	return RemoteMethods{d: inst}
  1023  }
  1024  
  1025  // Search returns the SearchMethods that Instance has registered
  1026  func (inst *Instance) Search() SearchMethods {
  1027  	return SearchMethods{d: inst}
  1028  }
  1029  
  1030  // WithSource returns a wrapped instance that will resolve refs from the given source
  1031  func (inst *Instance) WithSource(source string) *InstanceSourceWrap {
  1032  	return &InstanceSourceWrap{
  1033  		source: source,
  1034  		inst:   inst,
  1035  	}
  1036  }
  1037  
  1038  // InstanceSourceWrap is a wrapped instance with an explicit resolver source added
  1039  // TODO(dustmop): This struct is a temporary solution. The better approach is to
  1040  // make it easy to copy the Instance cheaply. All of Instance's "heavy" state, such
  1041  // as the Bus, and Logbook, should live on a shared object, while values that can
  1042  // be overwritten should live as separate fields. Then `WithSource` can be replaced
  1043  // with a method that constructs a new Instance that points to the original shared
  1044  // object, with other fields assigned as needed.
  1045  type InstanceSourceWrap struct {
  1046  	source string
  1047  	inst   *Instance
  1048  }
  1049  
  1050  // Automation returns the AutomationMethods that Instance has registered
  1051  func (isw *InstanceSourceWrap) Automation() AutomationMethods {
  1052  	return AutomationMethods{d: isw}
  1053  }
  1054  
  1055  // Dataset returns the DatasetMethods that Instance has registered
  1056  func (isw *InstanceSourceWrap) Dataset() DatasetMethods {
  1057  	return DatasetMethods{d: isw}
  1058  }
  1059  
  1060  // Log returns the LogMethods that Instance has registered
  1061  func (isw *InstanceSourceWrap) Log() LogMethods {
  1062  	return LogMethods{d: isw}
  1063  }
  1064  
  1065  // Remote returns the RemoteMethods that Instance has registered
  1066  func (isw *InstanceSourceWrap) Remote() RemoteMethods {
  1067  	return RemoteMethods{d: isw}
  1068  }
  1069  
  1070  // GetConfig provides methods for manipulating Qri configuration
  1071  //
  1072  // Deprecated: this method will be removed in a future release.
  1073  // Use inst.Config().GetConfig instead
  1074  func (inst *Instance) GetConfig() *config.Config {
  1075  	if inst == nil {
  1076  		return nil
  1077  	}
  1078  	return inst.cfg
  1079  }
  1080  
  1081  // Shutdown closes the instance, releasing all held resources. the returned
  1082  // channel will write any closing error, including context cancellation
  1083  // timeout
  1084  func (inst *Instance) Shutdown() <-chan error {
  1085  	errCh := make(chan error)
  1086  	// NOTE: the remote client may have gotten its context from the `Connect` func
  1087  	// not the context that the instance itself was built around.
  1088  	// The instance must clean up the remoteClient, since it cannot rely on the
  1089  	// remote client's context to cancel at the same time as the instance's context
  1090  	if inst.remoteClient != nil {
  1091  		<-inst.remoteClient.Shutdown()
  1092  	}
  1093  	// NOTE: when the QriNode goes "Online" it creates a new context, like the
  1094  	// above remote client, we have to explicitly "GoOffline" in order to make
  1095  	// sure we are releasing all resources
  1096  	inst.node.GoOffline()
  1097  	go func() {
  1098  		<-inst.doneCh
  1099  		errCh <- inst.doneErr
  1100  	}()
  1101  	inst.cancel()
  1102  	return errCh
  1103  }
  1104  
  1105  // ChangeConfig implements the ConfigSetter interface
  1106  func (inst *Instance) ChangeConfig(cfg *config.Config) (err error) {
  1107  	cfg = cfg.WithPrivateValues(inst.cfg)
  1108  
  1109  	if path := inst.cfg.Path(); path != "" {
  1110  		if err = cfg.WriteToFile(path); err != nil {
  1111  			return
  1112  		}
  1113  	}
  1114  
  1115  	inst.cfg = cfg
  1116  	return nil
  1117  }
  1118  
  1119  // Node accesses the instance qri node if one exists
  1120  func (inst *Instance) Node() *p2p.QriNode {
  1121  	if inst == nil {
  1122  		return nil
  1123  	}
  1124  	return inst.node
  1125  }
  1126  
  1127  // Repo accesses the instance Repo if one exists
  1128  func (inst *Instance) Repo() repo.Repo {
  1129  	if inst == nil {
  1130  		return nil
  1131  	}
  1132  	if inst.repo != nil {
  1133  		return inst.repo
  1134  	} else if inst.node != nil {
  1135  		return inst.node.Repo
  1136  	}
  1137  	return nil
  1138  }
  1139  
  1140  // RepoPath returns the path to the directory qri is operating from
  1141  func (inst *Instance) RepoPath() string {
  1142  	if inst == nil {
  1143  		return ""
  1144  	}
  1145  	return inst.repoPath
  1146  }
  1147  
  1148  // Dscache returns the dscache that the instance has
  1149  func (inst *Instance) Dscache() *dscache.Dscache {
  1150  	if inst == nil {
  1151  		return nil
  1152  	}
  1153  	return inst.dscache
  1154  }
  1155  
  1156  // HTTPClient accesses the instance HTTP client if one exists
  1157  func (inst *Instance) HTTPClient() *qhttp.Client {
  1158  	if inst == nil {
  1159  		return nil
  1160  	}
  1161  	return inst.http
  1162  }
  1163  
  1164  // RemoteServer accesses the remote subsystem if one exists
  1165  func (inst *Instance) RemoteServer() *remote.Server {
  1166  	if inst == nil {
  1167  		return nil
  1168  	}
  1169  	return inst.remoteServer
  1170  }
  1171  
  1172  // RemoteClient exposes the instance client for making requests to remotes
  1173  func (inst *Instance) RemoteClient() remote.Client {
  1174  	if inst == nil {
  1175  		return nil
  1176  	}
  1177  	return inst.remoteClient
  1178  }
  1179  
  1180  // Bus exposes the instance event bus
  1181  func (inst *Instance) Bus() event.Bus {
  1182  	if inst == nil {
  1183  		return nil
  1184  	}
  1185  	return inst.bus
  1186  }
  1187  
  1188  // TokenProvider exposes the instance token provider
  1189  func (inst *Instance) TokenProvider() token.Provider {
  1190  	if inst == nil {
  1191  		return nil
  1192  	}
  1193  	return inst.tokenProvider
  1194  }
  1195  
  1196  // KeyStore exposes the instance key.Store
  1197  func (inst *Instance) KeyStore() key.Store {
  1198  	if inst == nil {
  1199  		return nil
  1200  	}
  1201  	return inst.keystore
  1202  }
  1203  
  1204  // activeProfile tries to extract the current user from values embedded in the
  1205  // passed-in context, falling back to the repo owner as a default active profile
  1206  func (inst *Instance) activeProfile(ctx context.Context) (pro *profile.Profile, err error) {
  1207  	if inst == nil {
  1208  		return nil, fmt.Errorf("no instance")
  1209  	}
  1210  
  1211  	// try to get the profileID from the context
  1212  	profileIDString := profile.IDFromCtx(ctx)
  1213  	if profileIDString == "" {
  1214  		if tokenString := token.FromCtx(ctx); tokenString != "" {
  1215  			tok, err := token.ParseAuthToken(ctx, tokenString, inst.keystore)
  1216  			if err != nil {
  1217  				return nil, err
  1218  			}
  1219  
  1220  			if claims, ok := tok.Claims.(*token.Claims); ok {
  1221  				// TODO(b5): at this point we have a valid signature of a profileID string
  1222  				// but no proof that this profile is owned by the key that signed the
  1223  				// token. We either need ProfileID == KeyID, or we need a UCAN. we need to
  1224  				// check for those, ideally in a method within the profile package that
  1225  				// abstracts over profile & key agreement
  1226  				profileIDString = claims.Subject
  1227  			}
  1228  		}
  1229  	}
  1230  
  1231  	if profileIDString != "" {
  1232  		pid, err := profile.IDB58Decode(profileIDString)
  1233  		if err != nil {
  1234  			log.Errorw("profile", "id", profileIDString)
  1235  			return nil, fmt.Errorf("invalid profile ID")
  1236  		}
  1237  		pro, err := inst.profiles.GetProfile(ctx, pid)
  1238  		if errors.Is(err, profile.ErrNotFound) {
  1239  			return nil, fmt.Errorf("profile not found")
  1240  		}
  1241  		return pro, err
  1242  	}
  1243  
  1244  	if inst.profiles != nil {
  1245  		return inst.profiles.Owner(ctx), nil
  1246  	}
  1247  
  1248  	return nil, fmt.Errorf("no active profile")
  1249  }
  1250  
  1251  // checkRPCError validates RPC errors and in case of EOF returns a
  1252  // more user friendly message
  1253  func checkRPCError(err error) error {
  1254  	if err == nil {
  1255  		return nil
  1256  	}
  1257  	if strings.Contains(err.Error(), "EOF") {
  1258  		msg := `Qri couldn't parse the response and is unsure if it was successful. 
  1259  It is possible you have a Qri node running or the Desktop app is open.
  1260  Try closing them and running the command again.
  1261  Check our issue tracker for RPC issues & feature requests:
  1262    https://github.com/qri-io/qri/issues?q=is:issue+label:RPC
  1263  
  1264  Error:
  1265  %s`
  1266  		return qrierr.New(err, fmt.Sprintf(msg, err.Error()))
  1267  	}
  1268  	return err
  1269  }
  1270  
  1271  func (inst *Instance) waitForAllDone() {
  1272  	inst.releasers.Wait()
  1273  	log.Debug("closing instance")
  1274  	close(inst.doneCh)
  1275  }