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