Add check for app update (#88)

* add check for app update; fix ETUI error handling

* validate user args
This commit is contained in:
Alex Goodman
2020-07-21 12:02:03 -04:00
committed by GitHub
parent 380cd39514
commit 7ebb9f4e0b
19 changed files with 438 additions and 66 deletions
+10 -8
View File
@@ -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) {
+24 -8
View File
@@ -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
}
+17
View File
@@ -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
}
+3 -6
View File
@@ -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
View File
@@ -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
+36
View File
@@ -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,
}
}
+68
View File
@@ -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)
}
+202
View File
@@ -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())
}
})
}
}