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  }