github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/cmd/syft/cli/commands/update.go (about) 1 package commands 2 3 import ( 4 "fmt" 5 "io" 6 "net/http" 7 "strings" 8 9 "github.com/spf13/cobra" 10 "github.com/wagoodman/go-partybus" 11 12 "github.com/anchore/clio" 13 hashiVersion "github.com/anchore/go-version" 14 "github.com/anchore/syft/cmd/syft/cli/options" 15 "github.com/anchore/syft/cmd/syft/internal" 16 "github.com/anchore/syft/internal/bus" 17 "github.com/anchore/syft/internal/log" 18 "github.com/anchore/syft/syft/event" 19 "github.com/anchore/syft/syft/event/parsers" 20 ) 21 22 var latestAppVersionURL = struct { 23 host string 24 path string 25 }{ 26 host: "https://toolbox-data.anchore.io", 27 path: "/syft/releases/latest/VERSION", 28 } 29 30 func applicationUpdateCheck(id clio.Identification, check *options.UpdateCheck) func(cmd *cobra.Command, args []string) error { 31 return func(cmd *cobra.Command, args []string) error { 32 if check.CheckForAppUpdate { 33 checkForApplicationUpdate(id) 34 } 35 return nil 36 } 37 } 38 39 func checkForApplicationUpdate(id clio.Identification) { 40 log.Debugf("checking if a new version of %s is available", id.Name) 41 isAvailable, newVersion, err := isUpdateAvailable(id) 42 if err != nil { 43 // this should never stop the application 44 log.Errorf(err.Error()) 45 } 46 if isAvailable { 47 log.Infof("new version of %s is available: %s (current version is %s)", id.Name, newVersion, id.Version) 48 49 bus.Publish(partybus.Event{ 50 Type: event.CLIAppUpdateAvailable, 51 Value: parsers.UpdateCheck{ 52 New: newVersion, 53 Current: id.Version, 54 }, 55 }) 56 } else { 57 log.Debugf("no new %s update available", id.Name) 58 } 59 } 60 61 // isUpdateAvailable indicates if there is a newer application version available, and if so, what the new version is. 62 func isUpdateAvailable(id clio.Identification) (bool, string, error) { 63 if !isProductionBuild(id.Version) { 64 // don't allow for non-production builds to check for a version. 65 return false, "", nil 66 } 67 68 currentVersion, err := hashiVersion.NewVersion(id.Version) 69 if err != nil { 70 return false, "", fmt.Errorf("failed to parse current application version: %w", err) 71 } 72 73 latestVersion, err := fetchLatestApplicationVersion(id) 74 if err != nil { 75 return false, "", err 76 } 77 78 if latestVersion.GreaterThan(currentVersion) { 79 return true, latestVersion.String(), nil 80 } 81 82 return false, "", nil 83 } 84 85 func isProductionBuild(version string) bool { 86 if strings.Contains(version, "SNAPSHOT") || strings.Contains(version, internal.NotProvided) { 87 return false 88 } 89 return true 90 } 91 92 func fetchLatestApplicationVersion(id clio.Identification) (*hashiVersion.Version, error) { 93 req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil) 94 if err != nil { 95 return nil, fmt.Errorf("failed to create request for latest version: %w", err) 96 } 97 req.Header.Add("User-Agent", fmt.Sprintf("%v %v", id.Name, id.Version)) 98 99 client := http.Client{} 100 resp, err := client.Do(req) 101 if err != nil { 102 return nil, fmt.Errorf("failed to fetch latest version: %w", err) 103 } 104 defer func() { _ = resp.Body.Close() }() 105 106 if resp.StatusCode != http.StatusOK { 107 return nil, fmt.Errorf("HTTP %d on fetching latest version: %s", resp.StatusCode, resp.Status) 108 } 109 110 versionBytes, err := io.ReadAll(resp.Body) 111 if err != nil { 112 return nil, fmt.Errorf("failed to read latest version: %w", err) 113 } 114 115 versionStr := strings.TrimSuffix(string(versionBytes), "\n") 116 if len(versionStr) > 50 { 117 return nil, fmt.Errorf("version too long: %q", versionStr[:50]) 118 } 119 120 return hashiVersion.NewVersion(versionStr) 121 }