github.com/criteo/command-launcher@v0.0.0-20230407142452-fb616f546e98/internal/updater/cmd-updater.go (about)

     1  package updater
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/criteo/command-launcher/internal/console"
    10  	"github.com/criteo/command-launcher/internal/helper"
    11  	"github.com/criteo/command-launcher/internal/remote"
    12  	"github.com/criteo/command-launcher/internal/repository"
    13  	"github.com/criteo/command-launcher/internal/user"
    14  
    15  	log "github.com/sirupsen/logrus"
    16  )
    17  
    18  type CmdUpdater struct {
    19  	cmdUpdateChan <-chan bool
    20  
    21  	initRemoteRepoOnce sync.Once
    22  	remoteRepo         remote.RemoteRepository
    23  	initRemoteRepoErr  error
    24  
    25  	toBeDeleted   map[string]string
    26  	toBeUpdated   map[string]string
    27  	toBeInstalled map[string]string
    28  
    29  	CmdRepositoryBaseUrl string
    30  	LocalRepo            repository.PackageRepository
    31  	User                 user.User
    32  	Timeout              time.Duration
    33  	EnableCI             bool
    34  	PackageLockFile      string
    35  	VerifyChecksum       bool
    36  	VerifySignature      bool
    37  }
    38  
    39  func (u *CmdUpdater) CheckUpdateAsync() {
    40  	ch := make(chan bool, 1)
    41  	u.cmdUpdateChan = ch
    42  	go func() {
    43  		select {
    44  		case value := <-u.checkUpdateCommands():
    45  			ch <- value
    46  		case <-time.After(u.Timeout):
    47  			ch <- false
    48  		}
    49  	}()
    50  }
    51  
    52  func (u *CmdUpdater) Update() error {
    53  	canBeUpdated := <-u.cmdUpdateChan
    54  	if !canBeUpdated {
    55  		return nil
    56  	}
    57  
    58  	errPool := []error{}
    59  
    60  	remoteRepo, err := u.getRemoteRepository()
    61  	if err != nil {
    62  		// TODO: handle error here
    63  		return err
    64  	}
    65  
    66  	fmt.Println("\n-----------------------------------")
    67  	fmt.Println("Some commands require update, please wait...")
    68  	repo := u.LocalRepo
    69  
    70  	// first delete deprecated packages
    71  	if u.toBeDeleted != nil && len(u.toBeDeleted) > 0 {
    72  		for pkg := range u.toBeDeleted {
    73  			console.Highlight("- remove deprecated package '%s', it will not be available from now on\n", pkg)
    74  			if err = repo.Uninstall(pkg); err != nil {
    75  				errPool = append(errPool, err)
    76  				fmt.Printf("Cannot uninstall the package %s: %v\n", pkg, err)
    77  			}
    78  		}
    79  	}
    80  
    81  	// update existing pacakges
    82  	if u.toBeUpdated != nil && len(u.toBeUpdated) > 0 {
    83  		for pkgName, remoteVersion := range u.toBeUpdated {
    84  			localPkg, err := u.LocalRepo.Package(pkgName)
    85  			if err != nil {
    86  				errPool = append(errPool, err)
    87  				continue
    88  			}
    89  			op := "upgrade"
    90  			if remote.IsVersionSmaller(remoteVersion, localPkg.Version()) {
    91  				op = "downgrade"
    92  			}
    93  			console.Highlight("- %s command '%s' from version %s to version %s ...\n", op, pkgName, localPkg.Version(), remoteVersion)
    94  			pkg, err := remoteRepo.Package(pkgName, remoteVersion)
    95  			if err != nil {
    96  				errPool = append(errPool, err)
    97  				fmt.Printf("Cannot get the package %s: %v\n", pkgName, err)
    98  				continue
    99  			}
   100  			if ok, err := remoteRepo.Verify(pkg, u.VerifyChecksum, u.VerifySignature); !ok || err != nil {
   101  				errPool = append(errPool, err)
   102  				fmt.Printf("Failed to verify package %s, skip it: %v\n", pkgName, err)
   103  				continue
   104  			}
   105  			if err = repo.Update(pkg); err != nil {
   106  				errPool = append(errPool, err)
   107  				fmt.Printf("Cannot update the command %s: %v\n", pkgName, err)
   108  			}
   109  		}
   110  	}
   111  
   112  	// install new ones
   113  	if u.toBeInstalled != nil && len(u.toBeInstalled) > 0 {
   114  		for pkgName, remoteVersion := range u.toBeInstalled {
   115  			_, err = repo.Package(pkgName)
   116  			if err != nil { // only install package that doesn't exist locally
   117  				console.Highlight("- install new package '%s'\n", pkgName)
   118  				pkg, err := remoteRepo.Package(pkgName, remoteVersion)
   119  				if err != nil {
   120  					errPool = append(errPool, err)
   121  					fmt.Printf("Cannot get the package %s: %v\n", pkgName, err)
   122  					continue
   123  				}
   124  				if ok, err := remoteRepo.Verify(pkg, u.VerifyChecksum, u.VerifySignature); !ok || err != nil {
   125  					errPool = append(errPool, err)
   126  					fmt.Printf("Failed to verify package %s, skip it: %v\n", pkgName, err)
   127  					continue
   128  				}
   129  				if err = repo.Install(pkg); err != nil {
   130  					errPool = append(errPool, err)
   131  					fmt.Printf("Cannot install the package %s: %v\n", pkgName, err)
   132  				}
   133  			} else {
   134  				errPool = append(errPool,
   135  					fmt.Errorf("Package %s already exists in your local registry, you probably have a corrupted local registry", pkgName))
   136  			}
   137  		}
   138  	}
   139  
   140  	if len(errPool) == 0 {
   141  		fmt.Println("Update done! Enjoy coding!")
   142  		return nil
   143  	} else {
   144  		return errPool[0]
   145  	}
   146  }
   147  
   148  func (u *CmdUpdater) checkUpdateCommands() <-chan bool {
   149  	ch := make(chan bool, 1)
   150  	canBeUpdated := false
   151  	go func() {
   152  		remoteRepo, err := u.getRemoteRepository()
   153  		if err != nil {
   154  			canBeUpdated = false
   155  			ch <- canBeUpdated
   156  			return
   157  		}
   158  
   159  		install := map[string]string{}
   160  		update := map[string]string{}
   161  		delete := map[string]string{}
   162  
   163  		// find all available package for this user's partition
   164  		availablePkgs := map[string]string{}
   165  		if remotePkgNames, err := remoteRepo.PackageNames(); err == nil {
   166  			for _, remotePkgName := range remotePkgNames {
   167  				latest, err := remoteRepo.QueryLatestPackageInfo(remotePkgName, func(pkgInfo *remote.PackageInfo) bool {
   168  					return u.User.InPartition(pkgInfo.StartPartition, pkgInfo.EndPartition)
   169  				})
   170  				if err != nil {
   171  					continue
   172  				}
   173  				availablePkgs[latest.Name] = latest.Version
   174  			}
   175  		}
   176  
   177  		if u.EnableCI {
   178  			log.Infoln("CI mode enabled")
   179  			if lockedPkgs, err := u.LoadLockedPackages(u.PackageLockFile); err == nil && len(lockedPkgs) > 0 {
   180  				log.Infof("checking locked packages from %s ...", u.PackageLockFile)
   181  				// check if the locked packages are in the remote registry
   182  				for k, v := range lockedPkgs {
   183  					log.Infof("package %s is locked to version %s", k, v)
   184  					if _, ok := availablePkgs[k]; !ok {
   185  						log.Infoln(fmt.Errorf("package %s@%s is not available on the remote registry", k, v))
   186  						canBeUpdated = false
   187  						ch <- canBeUpdated
   188  						return
   189  					}
   190  					// TODO: check if the locked version exists
   191  				}
   192  				// now set available packages to the locked ones
   193  				availablePkgs = lockedPkgs
   194  			} else if err != nil {
   195  				log.Errorln(err)
   196  			} else {
   197  				log.Infof("Empty lock file %s", u.PackageLockFile)
   198  			}
   199  		}
   200  
   201  		// iterate local packages to find to be deleted and to be updated ones
   202  		// delete : exist in local, but not in remote
   203  		// update: exist both in local and remote, but different versions
   204  		localPkgMap := map[string]string{}
   205  		localPkgs := u.LocalRepo.InstalledPackages()
   206  		for _, localPkg := range localPkgs {
   207  			localPkgMap[localPkg.Name()] = localPkg.Version()
   208  			if remoteVersion, exist := availablePkgs[localPkg.Name()]; exist {
   209  				if remoteVersion != localPkg.Version() {
   210  					// to be updated
   211  					update[localPkg.Name()] = remoteVersion
   212  				}
   213  			} else {
   214  				// to be deleted
   215  				delete[localPkg.Name()] = localPkg.Version()
   216  			}
   217  		}
   218  
   219  		// iterate the available pacakge again to find to be newly installed ones
   220  		// (exist in remote available pkgs, but not in local packages)
   221  		for pkg, version := range availablePkgs {
   222  			if _, exist := localPkgMap[pkg]; !exist {
   223  				install[pkg] = version
   224  			}
   225  		}
   226  
   227  		u.toBeDeleted = delete
   228  		u.toBeUpdated = update
   229  		u.toBeInstalled = install
   230  
   231  		if len(u.toBeDeleted) > 0 || len(u.toBeUpdated) > 0 || len(u.toBeInstalled) > 0 {
   232  			canBeUpdated = true
   233  		} else {
   234  			canBeUpdated = false
   235  		}
   236  
   237  		ch <- canBeUpdated
   238  	}()
   239  
   240  	return ch
   241  }
   242  
   243  // only fetch remote repository once in each updater instance
   244  func (u *CmdUpdater) getRemoteRepository() (remote.RemoteRepository, error) {
   245  	if u.CmdRepositoryBaseUrl == "" {
   246  		return nil, fmt.Errorf("invalid remote repository url")
   247  	}
   248  	u.initRemoteRepoOnce.Do(func() {
   249  		u.remoteRepo = remote.CreateRemoteRepository(u.CmdRepositoryBaseUrl)
   250  		u.initRemoteRepoErr = u.remoteRepo.Fetch()
   251  	})
   252  	return u.remoteRepo, u.initRemoteRepoErr
   253  }
   254  
   255  // load the package lock file
   256  func (u *CmdUpdater) LoadLockedPackages(lockFile string) (map[string]string, error) {
   257  	lockedPkgs := map[string]string{}
   258  	content, err := helper.LoadFile(lockFile)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	if err := json.Unmarshal(content, &lockedPkgs); err != nil {
   264  		return nil, err
   265  	}
   266  	return lockedPkgs, nil
   267  }