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  }