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 }