mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 15:06:36 +01:00
Add check for app update (#88)
* add check for app update; fix ETUI error handling * validate user args
This commit is contained in:
@@ -20,14 +20,15 @@ type CliOnlyOptions struct {
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
ConfigPath string
|
||||
PresenterOpt presenter.Option
|
||||
Output string `mapstructure:"output"`
|
||||
ScopeOpt scope.Option
|
||||
Scope string `mapstructure:"scope"`
|
||||
Quiet bool `mapstructure:"quiet"`
|
||||
Log Logging `mapstructure:"log"`
|
||||
CliOptions CliOnlyOptions
|
||||
ConfigPath string
|
||||
PresenterOpt presenter.Option
|
||||
Output string `mapstructure:"output"`
|
||||
ScopeOpt scope.Option
|
||||
Scope string `mapstructure:"scope"`
|
||||
Quiet bool `mapstructure:"quiet"`
|
||||
Log Logging `mapstructure:"log"`
|
||||
CliOptions CliOnlyOptions
|
||||
CheckForAppUpdate bool `mapstructure:"check-for-app-update"`
|
||||
}
|
||||
|
||||
type Logging struct {
|
||||
@@ -41,6 +42,7 @@ func setNonCliDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("log.level", "")
|
||||
v.SetDefault("log.file", "")
|
||||
v.SetDefault("log.structured", false)
|
||||
v.SetDefault("check-for-app-update", true)
|
||||
}
|
||||
|
||||
func LoadConfigFromFile(v *viper.Viper, cliOpts *CliOnlyOptions) (*Application, error) {
|
||||
|
||||
@@ -32,7 +32,7 @@ func setupScreen(output *os.File) *frame.Frame {
|
||||
}
|
||||
|
||||
// nolint:funlen,gocognit
|
||||
func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscription) int {
|
||||
func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
|
||||
output := os.Stderr
|
||||
|
||||
// hide cursor
|
||||
@@ -42,8 +42,14 @@ func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscr
|
||||
|
||||
fr := setupScreen(output)
|
||||
if fr == nil {
|
||||
return 1
|
||||
return fmt.Errorf("unable to setup screen")
|
||||
}
|
||||
var isClosed bool
|
||||
defer func() {
|
||||
if !isClosed {
|
||||
frame.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var err error
|
||||
var wg = &sync.WaitGroup{}
|
||||
@@ -53,39 +59,49 @@ func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscr
|
||||
eventLoop:
|
||||
for {
|
||||
select {
|
||||
case err := <-workerErrs:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case e, ok := <-events:
|
||||
if !ok {
|
||||
// is this unexpected? if so should we indicate this?
|
||||
break eventLoop
|
||||
}
|
||||
switch e.Type {
|
||||
case imgbomEvent.AppUpdateAvailable:
|
||||
err = appUpdateAvailableHandler(ctx, fr, e, wg)
|
||||
if err != nil {
|
||||
log.Errorf("unable to show AppUpdateAvailable event: %+v", err)
|
||||
}
|
||||
|
||||
case stereoscopeEvent.ReadImage:
|
||||
err = imageReadHandler(ctx, fr, e, wg)
|
||||
if err != nil {
|
||||
log.Errorf("unable to show read image event: %+v", err)
|
||||
log.Errorf("unable to show ReadImage event: %+v", err)
|
||||
}
|
||||
|
||||
case stereoscopeEvent.FetchImage:
|
||||
err = imageFetchHandler(ctx, fr, e, wg)
|
||||
if err != nil {
|
||||
log.Errorf("unable to show fetch image event: %+v", err)
|
||||
log.Errorf("unable to show FetchImage event: %+v", err)
|
||||
}
|
||||
|
||||
case imgbomEvent.CatalogerStarted:
|
||||
err = catalogerStartedHandler(ctx, fr, e, wg)
|
||||
if err != nil {
|
||||
log.Errorf("unable to show catalog image start event: %+v", err)
|
||||
log.Errorf("unable to show CatalogerStarted event: %+v", err)
|
||||
}
|
||||
case imgbomEvent.CatalogerFinished:
|
||||
// we may have other background processes still displaying progress, wait for them to
|
||||
// finish before discontinuing dynamic content and showing the final report
|
||||
wg.Wait()
|
||||
frame.Close()
|
||||
isClosed = true
|
||||
fmt.Println()
|
||||
|
||||
err := common.CatalogerFinishedHandler(e)
|
||||
if err != nil {
|
||||
log.Errorf("unable to show catalog image finished event: %+v", err)
|
||||
log.Errorf("unable to show CatalogerFinished event: %+v", err)
|
||||
}
|
||||
|
||||
// this is the last expected event
|
||||
@@ -99,5 +115,5 @@ eventLoop:
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -143,3 +143,20 @@ func catalogerStartedHandler(ctx context.Context, fr *frame.Frame, event partybu
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func appUpdateAvailableHandler(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error {
|
||||
newVersion, err := imgbomEventParsers.ParseAppUpdateAvailable(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad AppUpdateAvailable event: %w", err)
|
||||
}
|
||||
|
||||
line, err := fr.Prepend()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
message := color.Magenta.Sprintf("New Update Available: %s", newVersion)
|
||||
_, _ = io.WriteString(line, message)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,17 +7,14 @@ import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
func LoggerUI(workerErrs <-chan error, subscription *partybus.Subscription) int {
|
||||
var returnCode int
|
||||
|
||||
func LoggerUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
|
||||
events := subscription.Events()
|
||||
eventLoop:
|
||||
for {
|
||||
select {
|
||||
case err := <-workerErrs:
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
returnCode = 1
|
||||
return err
|
||||
}
|
||||
case e, ok := <-events:
|
||||
if !ok {
|
||||
@@ -38,5 +35,5 @@ eventLoop:
|
||||
}
|
||||
}
|
||||
|
||||
return returnCode
|
||||
return nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
type UI func(<-chan error, *partybus.Subscription) int
|
||||
type UI func(<-chan error, *partybus.Subscription) error
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
const valueNotProvided = "[not provided]"
|
||||
|
||||
var version = valueNotProvided
|
||||
var gitCommit = valueNotProvided
|
||||
var gitTreeState = valueNotProvided
|
||||
var buildDate = valueNotProvided
|
||||
var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
type Version struct {
|
||||
Version string
|
||||
GitCommit string
|
||||
GitTreeState string
|
||||
BuildDate string
|
||||
GoVersion string
|
||||
Compiler string
|
||||
Platform string
|
||||
}
|
||||
|
||||
func FromBuild() Version {
|
||||
return Version{
|
||||
Version: version,
|
||||
GitCommit: gitCommit,
|
||||
GitTreeState: gitTreeState,
|
||||
BuildDate: buildDate,
|
||||
GoVersion: runtime.Version(),
|
||||
Compiler: runtime.Compiler,
|
||||
Platform: platform,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
hashiVersion "github.com/anchore/go-version"
|
||||
)
|
||||
|
||||
var latestAppVersionURL = struct {
|
||||
host string
|
||||
path string
|
||||
}{
|
||||
// TODO: set me to release host/path before release
|
||||
host: "https://anchore.io",
|
||||
path: "/imgbom/releases/latest/VERSION",
|
||||
}
|
||||
|
||||
func IsUpdateAvailable() (bool, string, error) {
|
||||
currentVersionStr := FromBuild().Version
|
||||
currentVersion, err := hashiVersion.NewVersion(currentVersionStr)
|
||||
if err != nil {
|
||||
if currentVersionStr == valueNotProvided {
|
||||
// this is the default build arg and should be ignored (this is not an error case)
|
||||
return false, "", nil
|
||||
}
|
||||
return false, "", fmt.Errorf("failed to parse current application version: %w", err)
|
||||
}
|
||||
|
||||
latestVersion, err := fetchLatestApplicationVersion()
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
if latestVersion.GreaterThan(currentVersion) {
|
||||
return true, latestVersion.String(), nil
|
||||
}
|
||||
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
func fetchLatestApplicationVersion() (*hashiVersion.Version, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request for latest version: %w", err)
|
||||
}
|
||||
|
||||
client := http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch latest version: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP %d on fetching latest version: %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
versionBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read latest version: %w", err)
|
||||
}
|
||||
|
||||
versionStr := strings.TrimSuffix(string(versionBytes), "\n")
|
||||
return hashiVersion.NewVersion(versionStr)
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
hashiVersion "github.com/anchore/go-version"
|
||||
)
|
||||
|
||||
func TestIsUpdateAvailable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
buildVersion string
|
||||
latestVersion string
|
||||
code int
|
||||
isAvailable bool
|
||||
newVersion string
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
name: "equal",
|
||||
buildVersion: "1.0.0",
|
||||
latestVersion: "1.0.0",
|
||||
code: 200,
|
||||
isAvailable: false,
|
||||
newVersion: "",
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
name: "hasUpdate",
|
||||
buildVersion: "1.0.0",
|
||||
latestVersion: "1.2.0",
|
||||
code: 200,
|
||||
isAvailable: true,
|
||||
newVersion: "1.2.0",
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
name: "aheadOfLatest",
|
||||
buildVersion: "1.2.0",
|
||||
latestVersion: "1.0.0",
|
||||
code: 200,
|
||||
isAvailable: false,
|
||||
newVersion: "",
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
name: "EmptyUpdate",
|
||||
buildVersion: "1.0.0",
|
||||
latestVersion: "",
|
||||
code: 200,
|
||||
isAvailable: false,
|
||||
newVersion: "",
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "GarbageUpdate",
|
||||
buildVersion: "1.0.0",
|
||||
latestVersion: "hdfjksdhfhkj",
|
||||
code: 200,
|
||||
isAvailable: false,
|
||||
newVersion: "",
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "BadUpdate",
|
||||
buildVersion: "1.0.0",
|
||||
latestVersion: "1.0.",
|
||||
code: 500,
|
||||
isAvailable: false,
|
||||
newVersion: "",
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "NoBuildVersion",
|
||||
buildVersion: valueNotProvided,
|
||||
latestVersion: "1.0.0",
|
||||
code: 200,
|
||||
isAvailable: false,
|
||||
newVersion: "",
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
name: "BadUpdateValidVersion",
|
||||
buildVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
code: 404,
|
||||
isAvailable: false,
|
||||
newVersion: "",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// setup mocks
|
||||
// local...
|
||||
version = test.buildVersion
|
||||
// remote...
|
||||
handler := http.NewServeMux()
|
||||
handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(test.code)
|
||||
_, _ = w.Write([]byte(test.latestVersion))
|
||||
})
|
||||
mockSrv := httptest.NewServer(handler)
|
||||
latestAppVersionURL.host = mockSrv.URL
|
||||
defer mockSrv.Close()
|
||||
|
||||
isAvailable, newVersion, err := IsUpdateAvailable()
|
||||
if err != nil && !test.err {
|
||||
t.Fatalf("got error but expected none: %+v", err)
|
||||
} else if err == nil && test.err {
|
||||
t.Fatalf("expected error but got none")
|
||||
}
|
||||
|
||||
if newVersion != test.newVersion {
|
||||
t.Errorf("unexpected NEW version: %+v", newVersion)
|
||||
}
|
||||
|
||||
if isAvailable != test.isAvailable {
|
||||
t.Errorf("unexpected result: %+v", isAvailable)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFetchLatestApplicationVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
response string
|
||||
code int
|
||||
err bool
|
||||
expected *hashiVersion.Version
|
||||
}{
|
||||
{
|
||||
name: "gocase",
|
||||
response: "1.0.0",
|
||||
code: 200,
|
||||
expected: hashiVersion.Must(hashiVersion.NewVersion("1.0.0")),
|
||||
},
|
||||
{
|
||||
name: "garbage",
|
||||
response: "garbage",
|
||||
code: 200,
|
||||
expected: nil,
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "http 500",
|
||||
response: "1.0.0",
|
||||
code: 500,
|
||||
expected: nil,
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "http 404",
|
||||
response: "1.0.0",
|
||||
code: 404,
|
||||
expected: nil,
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
response: "",
|
||||
code: 200,
|
||||
expected: nil,
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// setup mock
|
||||
handler := http.NewServeMux()
|
||||
handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(test.code)
|
||||
_, _ = w.Write([]byte(test.response))
|
||||
})
|
||||
mockSrv := httptest.NewServer(handler)
|
||||
latestAppVersionURL.host = mockSrv.URL
|
||||
defer mockSrv.Close()
|
||||
|
||||
actual, err := fetchLatestApplicationVersion()
|
||||
if err != nil && !test.err {
|
||||
t.Fatalf("got error but expected none: %+v", err)
|
||||
} else if err == nil && test.err {
|
||||
t.Fatalf("expected error but got none")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if actual.String() != test.expected.String() {
|
||||
t.Errorf("unexpected version: %+v", actual.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user