github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/app/installer.go (about) 1 package app 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/url" 10 "regexp" 11 "time" 12 13 semver "github.com/Masterminds/semver/v3" 14 "github.com/cozy/cozy-stack/model/instance" 15 "github.com/cozy/cozy-stack/model/permission" 16 "github.com/cozy/cozy-stack/pkg/appfs" 17 build "github.com/cozy/cozy-stack/pkg/config" 18 "github.com/cozy/cozy-stack/pkg/config/config" 19 "github.com/cozy/cozy-stack/pkg/consts" 20 "github.com/cozy/cozy-stack/pkg/couchdb" 21 "github.com/cozy/cozy-stack/pkg/logger" 22 "github.com/cozy/cozy-stack/pkg/prefixer" 23 "github.com/cozy/cozy-stack/pkg/realtime" 24 "github.com/cozy/cozy-stack/pkg/registry" 25 "github.com/cozy/cozy-stack/pkg/utils" 26 ) 27 28 var slugReg = regexp.MustCompile(`^[a-z0-9\-]+$`) 29 30 var ErrInvalidManifestTypes = errors.New("Manifest type is unknown") 31 var ErrInvalidManifestForWebapp = errors.New("Manifest type is not valid for a webapp. Maybe you want to install a konnector?") 32 var ErrInvalidManifestForKonnector = errors.New("Manifest type is not valid for a konnector. Maybe you want to install a webapp?") 33 34 // Operation is the type of operation the installer is created for. 35 type Operation int 36 37 const ( 38 // Install operation for installing an application 39 Install Operation = iota + 1 40 // Update operation for updating an application 41 Update 42 // Delete operation for deleting an application 43 Delete 44 ) 45 46 // Installer is used to install or update applications. 47 type Installer struct { 48 fetcher Fetcher 49 op Operation 50 fs appfs.Copier 51 db prefixer.Prefixer 52 endState State 53 54 overridenParameters map[string]interface{} 55 permissionsAcked bool 56 57 man Manifest 58 src *url.URL 59 slug string 60 context string 61 62 manc chan Manifest 63 log logger.Logger 64 } 65 66 // InstallerOptions provides the slug name of the application along with the 67 // source URL. 68 type InstallerOptions struct { 69 Type consts.AppType 70 Operation Operation 71 Manifest Manifest 72 Slug string 73 SourceURL string 74 Deactivated bool 75 PermissionsAcked bool 76 Registries []*url.URL 77 78 // Used to override the "Parameters" field of konnectors during installation. 79 // This modification is useful to allow the parameterization of a konnector 80 // at its installation as we do not have yet a registry up and running. 81 OverridenParameters map[string]interface{} 82 } 83 84 // Fetcher interface should be implemented by the underlying transport 85 // used to fetch the application data. 86 type Fetcher interface { 87 // FetchManifest should returns an io.ReadCloser to read the 88 // manifest data 89 FetchManifest(src *url.URL) (io.ReadCloser, error) 90 // Fetch should download the application and install it in the given 91 // directory. 92 Fetch(src *url.URL, fs appfs.Copier, man Manifest) error 93 } 94 95 // NewInstaller creates a new Installer 96 func NewInstaller(in *instance.Instance, fs appfs.Copier, opts *InstallerOptions) (*Installer, error) { 97 man, err := initManifest(in, opts) 98 if err != nil { 99 return nil, err 100 } 101 102 var src *url.URL 103 switch opts.Operation { 104 case Install: 105 if opts.SourceURL == "" { 106 return nil, ErrMissingSource 107 } 108 src, err = url.Parse(opts.SourceURL) 109 case Update, Delete: 110 var srcString string 111 if opts.SourceURL == "" { 112 srcString = man.Source() 113 } else { 114 srcString = opts.SourceURL 115 } 116 src, err = url.Parse(srcString) 117 default: 118 panic("Unknown installer operation") 119 } 120 if err != nil { 121 return nil, err 122 } 123 124 var endState State 125 if opts.Deactivated || man.State() == Installed { 126 endState = Installed 127 } else { 128 endState = Ready 129 } 130 131 var installType string 132 switch opts.Operation { 133 case Install: 134 installType = "install" 135 case Update: 136 installType = "update" 137 case Delete: 138 installType = "delete" 139 } 140 141 log := logger.WithDomain(in.DomainName()).WithFields(logger.Fields{ 142 "nspace": "apps", 143 "slug": man.Slug(), 144 "version_start": man.Version(), 145 "type": installType, 146 }) 147 148 var manFilename string 149 switch man.AppType() { 150 case consts.WebappType: 151 manFilename = WebappManifestName 152 case consts.KonnectorType: 153 manFilename = KonnectorManifestName 154 } 155 156 var fetcher Fetcher 157 switch src.Scheme { 158 case "git", "git+ssh", "ssh+git", "git+https": 159 fetcher = newGitFetcher(manFilename, log) 160 case "http", "https": 161 fetcher = newHTTPFetcher(manFilename, log) 162 case "registry": 163 fetcher = newRegistryFetcher(opts.Registries, log) 164 case "file": 165 fetcher = newFileFetcher(manFilename, log) 166 default: 167 return nil, ErrNotSupportedSource 168 } 169 170 return &Installer{ 171 fetcher: fetcher, 172 op: opts.Operation, 173 db: in, 174 fs: fs, 175 endState: endState, 176 177 overridenParameters: opts.OverridenParameters, 178 permissionsAcked: opts.PermissionsAcked, 179 180 man: man, 181 src: src, 182 slug: man.Slug(), 183 context: in.ContextName, 184 185 manc: make(chan Manifest, 2), 186 log: log, 187 }, nil 188 } 189 190 func initManifest(db prefixer.Prefixer, opts *InstallerOptions) (man Manifest, err error) { 191 if man = opts.Manifest; man != nil { 192 return man, nil 193 } 194 195 slug := opts.Slug 196 if slug == "" || !slugReg.MatchString(slug) { 197 return nil, ErrInvalidSlugName 198 } 199 200 if opts.Operation == Install { 201 _, err = GetBySlug(db, slug, opts.Type) 202 if err == nil { 203 return nil, ErrAlreadyExists 204 } 205 if !errors.Is(err, ErrNotFound) { 206 return nil, err 207 } 208 switch opts.Type { 209 case consts.WebappType: 210 man = &WebappManifest{ 211 doc: &couchdb.JSONDoc{ 212 Type: consts.Apps, 213 M: map[string]interface{}{ 214 "_id": consts.Apps + "/" + slug, 215 }, 216 }, 217 } 218 case consts.KonnectorType: 219 man = &KonnManifest{ 220 doc: &couchdb.JSONDoc{ 221 Type: consts.Konnectors, 222 M: map[string]interface{}{ 223 "_id": consts.Konnectors + "/" + slug, 224 }, 225 }, 226 } 227 } 228 man.SetSlug(slug) 229 } else { 230 man, err = GetBySlug(db, slug, opts.Type) 231 if err != nil { 232 return nil, err 233 } 234 } 235 236 if man == nil { 237 panic("Bad or missing installer type") 238 } 239 240 return man, nil 241 } 242 243 // Slug return the slug of the application being installed. 244 func (i *Installer) Slug() string { 245 return i.slug 246 } 247 248 // Domain return the domain of instance associated with the installer. 249 func (i *Installer) Domain() string { 250 return i.db.DomainName() 251 } 252 253 // Run will install, update or delete the application linked to the installer, 254 // depending on specified operation. It will report its progress or error (see 255 // Poll method) and should be run asynchronously inside a new goroutine: 256 // `go installer.Run()`. 257 func (i *Installer) Run() { 258 if err := i.run(); err != nil { 259 i.man.SetError(err) 260 realtime.GetHub().Publish(i.db, realtime.EventUpdate, i.man.Clone(), nil) 261 } 262 i.notifyChannel() 263 } 264 265 // RunSync does the same work as Run but can be used synchronously. 266 func (i *Installer) RunSync() (Manifest, error) { 267 i.manc = nil 268 if err := i.run(); err != nil { 269 return nil, err 270 } 271 return i.man.Clone().(Manifest), nil 272 } 273 274 func (i *Installer) run() (err error) { 275 if i.man == nil { 276 panic("Manifest is nil") 277 } 278 mu := config.Lock().ReadWrite(i.db, "app-"+i.man.Slug()) 279 if err = mu.Lock(); err != nil { 280 i.log.Errorf("Could not get lock: %s", err) 281 return err 282 } 283 defer func() { 284 mu.Unlock() 285 if err != nil { 286 i.log.Errorf("Could not commit installer process: %s", err) 287 } else { 288 i.log.Infof("Successful installer process: %s", i.man.Version()) 289 } 290 }() 291 i.log.Infof("Start installer process: %s", i.man.Version()) 292 switch i.op { 293 case Install: 294 return i.install() 295 case Update: 296 return i.update() 297 case Delete: 298 return i.delete() 299 default: 300 panic("Unknown operation") 301 } 302 } 303 304 // install will perform the installation of an application. It returns the 305 // freshly fetched manifest from the source along with a possible error in case 306 // the installation went wrong. 307 // 308 // Note that the fetched manifest is returned even if an error occurred while 309 // upgrading. 310 func (i *Installer) install() error { 311 newManifest, err := i.ReadManifest(Installing) 312 if err != nil { 313 i.log.Debugf("Could not read manifest") 314 return err 315 } 316 i.man = newManifest 317 i.sendRealtimeEvent() 318 i.notifyChannel() 319 if err := i.fetcher.Fetch(i.src, i.fs, i.man); err != nil { 320 i.log.Debugf("Could not fetch tarball") 321 return err 322 } 323 i.man.SetState(i.endState) 324 return i.man.Create(i.db) 325 } 326 327 // checkSkipPermissions checks if the instance contexts is configured to skip 328 // permissions 329 func (i *Installer) checkSkipPermissions() (bool, error) { 330 domain := i.Domain() 331 if domain == prefixer.UnknownDomainName { 332 return false, nil 333 } 334 335 inst, err := instance.Get(domain) 336 if err != nil { 337 return false, err 338 } 339 ctxSettings, ok := inst.SettingsContext() 340 if !ok { 341 return false, nil 342 } 343 344 sp, ok := ctxSettings["permissions_skip_verification"] 345 if !ok { 346 return false, nil 347 } 348 349 return sp.(bool), nil 350 } 351 352 // update will perform the update of an already installed application. It 353 // returns the freshly fetched manifest from the source along with a possible 354 // error in case the update went wrong. 355 // 356 // Note that the fetched manifest is returned even if an error occurred while 357 // upgrading. 358 func (i *Installer) update() error { 359 // Reload the manifest from the database. It was loaded before this 360 // goroutine obtains the lock, and it may happen that another goroutine has 361 // made an update between the first load and the lock obtention. 362 // 363 // The first read is made before the lock to make the happy path (the app 364 // is already up-to-date) faster. 365 if i.man.AppType() == consts.WebappType { 366 reloaded, err := GetWebappBySlug(i.db, i.man.Slug()) 367 if err != nil { 368 return err 369 } 370 i.man = reloaded 371 } else { 372 reloaded, err := GetKonnectorBySlug(i.db, i.man.Slug()) 373 if err != nil { 374 return err 375 } 376 i.man = reloaded 377 } 378 379 if err := i.checkState(i.man); err != nil { 380 return err 381 } 382 383 oldManifest := i.man 384 newManifest, err := i.ReadManifest(Upgrading) 385 if err != nil { 386 return err 387 } 388 389 if fetcher, ok := i.fetcher.(*registryFetcher); ok { 390 newManifest.SetVersion(fetcher.appVersion()) 391 } 392 393 // Fast path for registry:// and http:// sources: we do not need to go 394 // further in the case where the fetched manifest has the same version has 395 // the one in database. 396 // 397 // For git:// and file:// sources, it may be more complicated since we need 398 // to actually fetch the data to extract the exact version of the manifest. 399 makeUpdate := true 400 availableVersion := "" 401 switch i.src.Scheme { 402 case "registry", "http", "https": 403 makeUpdate = (newManifest.Version() != oldManifest.Version()) 404 } 405 406 // Check the possible permissions changes before updating. If the 407 // verifyPermissions flag is activated (for non manual updates for example), 408 // we cancel out the update and mark the UpdateAvailable field of the 409 // application instead of actually updating. 410 if build.IsDevRelease() { 411 // If we are in dev release we want to automatically accept any permissions set 412 // inside the manifest. This allows bypassing the authorization acceptation 413 // page, a tiresome step for local dev. 414 switch newManifest.AppType() { 415 case consts.WebappType: 416 err = permission.ForceWebapp(i.db, newManifest.Slug(), newManifest.Permissions()) 417 case consts.KonnectorType: 418 err = permission.ForceKonnector(i.db, newManifest.Slug(), newManifest.Permissions()) 419 default: 420 err = fmt.Errorf("invalid app type: %q", newManifest.AppType()) 421 } 422 if err != nil { 423 return fmt.Errorf("failed to force the permission set: %w", err) 424 } 425 } else if makeUpdate && !isPlatformApp(oldManifest, i.context) { 426 oldPermissions := oldManifest.Permissions() 427 newPermissions := newManifest.Permissions() 428 samePermissions := false 429 430 if newPermissions != nil && oldPermissions != nil { 431 samePermissions = newPermissions.HasSameRules(oldPermissions) 432 433 // XXX the stack can auto-update konnectors if only a permission of 434 // carbon copy or electronic safe is added, without asking 435 // permission from the user. 436 if !samePermissions && oldManifest.AppType() == consts.KonnectorType { 437 diff := permission.Diff(oldPermissions, newPermissions) 438 for _, rule := range diff { 439 if rule.Type == consts.CertifiedCarbonCopy || 440 rule.Type == consts.CertifiedElectronicSafe { 441 if !oldPermissions.RuleInSubset(rule) { 442 oldPermissions = append(oldPermissions, rule) 443 samePermissions = newPermissions.HasSameRules(oldPermissions) 444 } 445 } 446 } 447 } 448 } 449 450 if !samePermissions && !i.permissionsAcked { 451 // Check if we are going to skip the permissions 452 skip, err := i.checkSkipPermissions() 453 if err != nil { 454 return err 455 } 456 if !skip { 457 makeUpdate = false 458 availableVersion = newManifest.Version() 459 } 460 } 461 } 462 463 oldTermsVersion := oldManifest.Terms().Version 464 newTermsVersion := newManifest.Terms().Version 465 466 termsAdded := oldTermsVersion == "" && newTermsVersion != "" 467 termsUpdated := oldTermsVersion != newTermsVersion 468 469 if (termsAdded || termsUpdated) && !i.permissionsAcked { 470 makeUpdate = false 471 availableVersion = newManifest.Version() 472 } 473 474 extraPerms := permission.Set{} 475 var alteredPerms *permission.Permission 476 // The "extraPerms" set represents the post-install alterations of the 477 // permissions between the oldManifest and the current permissions. 478 // 479 // Even if makeUpdate is false, we are going to update the manifest document 480 // to set an AvailableVersion. In this case, the current webapp/konnector 481 // perms will be reapplied and custom ones will be lost if we don't rewrite 482 // them. 483 inst, err := instance.Get(i.Domain()) 484 if err == nil { 485 // Check if perms were added on the old manifest 486 if i.man.AppType() == consts.WebappType { 487 alteredPerms, err = permission.GetForWebapp(inst, i.man.Slug()) 488 } else if i.man.AppType() == consts.KonnectorType { 489 alteredPerms, err = permission.GetForKonnector(inst, i.man.Slug()) 490 } 491 if err != nil { 492 return err 493 } 494 } 495 496 if alteredPerms != nil { 497 extraPerms = permission.Diff(oldManifest.Permissions(), alteredPerms.Permissions) 498 } 499 500 if makeUpdate { 501 i.man = newManifest 502 i.sendRealtimeEvent() 503 i.notifyChannel() 504 if err := i.fetcher.Fetch(i.src, i.fs, i.man); err != nil { 505 return err 506 } 507 i.man.SetAvailableVersion("") 508 i.man.SetState(i.endState) 509 } else { 510 if i.man.AppType() == consts.WebappType { 511 i.man.(*WebappManifest).oldServices = i.man.(*WebappManifest).val.Services 512 } 513 i.man.SetSource(i.src) 514 if availableVersion != "" { 515 i.man.SetAvailableVersion(availableVersion) 516 } 517 i.sendRealtimeEvent() 518 i.notifyChannel() 519 } 520 521 return i.man.Update(i.db, extraPerms) 522 } 523 524 func (i *Installer) notifyChannel() { 525 if i.manc != nil { 526 i.manc <- i.man.Clone().(Manifest) 527 } 528 } 529 530 func (i *Installer) delete() error { 531 if err := i.checkState(i.man); err != nil { 532 return err 533 } 534 return i.man.Delete(i.db) 535 } 536 537 // checkState returns whether or not the manifest is in the right state to 538 // perform an update or deletion. 539 func (i *Installer) checkState(man Manifest) error { 540 state := man.State() 541 if state == Ready || state == Installed { 542 return nil 543 } 544 if time.Since(man.LastUpdate()) > 15*time.Minute { 545 return nil 546 } 547 return ErrBadState 548 } 549 550 // ReadManifest will fetch the manifest and read its JSON content into the 551 // passed manifest pointer. 552 // 553 // The State field of the manifest will be set to the specified state. 554 func (i *Installer) ReadManifest(state State) (Manifest, error) { 555 r, err := i.fetcher.FetchManifest(i.src) 556 if err != nil { 557 return nil, err 558 } 559 defer r.Close() 560 561 var buf bytes.Buffer 562 tee := io.TeeReader(r, &buf) 563 564 newManifest, err := i.man.ReadManifest(io.LimitReader(tee, ManifestMaxSize), i.slug, i.src.String()) 565 if err != nil { 566 return nil, err 567 } 568 newManifest.SetState(state) 569 570 set := newManifest.Permissions() 571 for _, rule := range set { 572 if err := permission.CheckDoctypeName(rule.Type, true); err != nil { 573 return nil, err 574 } 575 } 576 577 shouldOverrideParameters := (i.overridenParameters != nil && 578 i.man.AppType() == consts.KonnectorType && 579 i.src.Scheme != "registry") 580 if shouldOverrideParameters { 581 if m, ok := newManifest.(*KonnManifest); ok { 582 m.val.Parameters = i.overridenParameters 583 } 584 } 585 586 // Checking the new manifest apptype to prevent human mistakes (like asking 587 // a konnector installation instead of a webapp) 588 newAppType := struct { 589 AppType string `json:"type"` 590 }{} 591 592 var newManifestAppType consts.AppType 593 if err = json.Unmarshal(buf.Bytes(), &newAppType); err == nil { 594 if newAppType.AppType == "konnector" { 595 newManifestAppType = consts.KonnectorType 596 } 597 if newAppType.AppType == "webapp" { 598 newManifestAppType = consts.WebappType 599 } 600 } 601 602 appTypesEmpty := i.man.AppType() == 0 || newManifestAppType == 0 603 appTypesMismatch := i.man.AppType() != newManifestAppType 604 605 if !appTypesEmpty && appTypesMismatch { 606 var typeError error 607 switch i.man.AppType() { 608 case consts.KonnectorType: 609 typeError = ErrInvalidManifestForKonnector 610 case consts.WebappType: 611 typeError = ErrInvalidManifestForWebapp 612 default: 613 typeError = ErrInvalidManifestTypes 614 } 615 return nil, fmt.Errorf("[%s] %w", i.man.Slug(), typeError) 616 } 617 return newManifest, nil 618 } 619 620 func (i *Installer) sendRealtimeEvent() { 621 realtime.GetHub().Publish(i.db, realtime.EventUpdate, i.man.Clone(), nil) 622 } 623 624 // Poll should be used to monitor the progress of the Installer. 625 func (i *Installer) Poll() (Manifest, bool, error) { 626 man := <-i.manc 627 done := false 628 if s := man.State(); s == Ready || s == Installed || s == Errored { 629 done = true 630 } 631 return man, done, man.Error() 632 } 633 634 // ManifestChannel returns the channel that can be listened to get updates 635 // about the installer run. 636 func (i *Installer) ManifestChannel() chan Manifest { 637 return i.manc 638 } 639 640 // DoLazyUpdate tries to update an application before using it 641 func DoLazyUpdate(in *instance.Instance, man Manifest, copier appfs.Copier, registries []*url.URL) Manifest { 642 src, err := url.Parse(man.Source()) 643 if err != nil { 644 return man 645 } 646 647 if src.Scheme == "registry" { 648 var v *registry.Version 649 channel, _ := getRegistryChannel(src) 650 v, errv := registry.GetLatestVersion(man.Slug(), channel, registries) 651 if errv != nil { 652 return man 653 } 654 if v.Version == man.Version() { 655 // In some cases, if the source had been altered mutiples times, the app 656 // may currently be in a stale state. 657 658 // Example: 659 // - The version 1.0.0 of the "foobar" konnector is installed from 660 // "stable" channel 661 // - The use switches to "beta" channel, the version 1.0.1 is available, 662 // but with extra perms 663 // - The update is blocked because of these news perms, the 664 // "available_version" is set to 1.0.1, the user switches back to "stable" 665 // channel 666 // - We are now on a stale state, no new version is available, but an 667 // available_version is set 668 669 // We ensure that we are not in this stale state by removing the 670 // available version field from the manifest if the latest version is 671 // the same as the current version 672 if man.AvailableVersion() != "" { 673 man.SetAvailableVersion("") 674 _ = man.Update(in, nil) 675 } 676 return man 677 } 678 679 if man.AvailableVersion() != "" && v.Version == man.AvailableVersion() { 680 return man 681 } 682 if channel == "stable" && !IsMoreRecent(man.Version(), v.Version) { 683 return man 684 } 685 } 686 687 inst, err := NewInstaller(in, copier, &InstallerOptions{ 688 Operation: Update, 689 Manifest: man, 690 Registries: registries, 691 SourceURL: src.String(), 692 Type: man.AppType(), 693 Slug: man.Slug(), 694 PermissionsAcked: false, 695 }) 696 if err != nil { 697 return man 698 } 699 newman, err := inst.RunSync() 700 if err != nil { 701 return man 702 } 703 return newman 704 } 705 706 // IsMoreRecent returns true if b is greater than a 707 func IsMoreRecent(a, b string) bool { 708 vA, err := semver.NewVersion(a) 709 if err != nil { 710 return true 711 } 712 vB, err := semver.NewVersion(b) 713 if err != nil { 714 return false 715 } 716 return vB.GreaterThan(vA) 717 } 718 719 func isPlatformApp(man Manifest, contextName string) bool { 720 if man.AppType() != consts.WebappType { 721 return false 722 } 723 if utils.IsInArray(man.Slug(), consts.PlatformApps) { 724 return true 725 } 726 727 contexts := config.GetConfig().Contexts 728 if contexts == nil { 729 return false 730 } 731 context, ok := contexts[contextName].(map[string]interface{}) 732 if !ok { 733 context, ok = contexts[config.DefaultInstanceContext].(map[string]interface{}) 734 } 735 if !ok { 736 return false 737 } 738 additional, ok := context["additional_platform_apps"].([]interface{}) 739 if !ok { 740 return false 741 } 742 for _, slug := range additional { 743 if slug == man.Slug() { 744 return true 745 } 746 } 747 return false 748 }