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(®client.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 }