diff --git a/.gitignore b/.gitignore index 3c0f71f0d..bd5111c63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.server/ .vscode/ *.tar *.jar diff --git a/.golangci.yaml b/.golangci.yaml index 7247fb42f..1445395ff 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -22,7 +22,6 @@ linters: - gosimple - govet - ineffassign - - maligned - misspell - nakedret - nolintlint @@ -48,6 +47,7 @@ linters: # - gomnd # this is too aggressive # - interfacer # this is a good idea, but is no longer supported and is prone to false positives # - lll # without a way to specify per-line exception cases, this is not usable +# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations # - nestif # - testpackage # - wsl \ No newline at end of file diff --git a/cmd/cmd.go b/cmd/cmd.go index 2eb8a0c4e..fb3a2ba59 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -38,7 +38,7 @@ func init() { func Execute() { if err := rootCmd.Execute(); err != nil { - log.Errorf("could not start application: %w", err) + log.Errorf(err.Error()) os.Exit(1) } } diff --git a/cmd/root.go b/cmd/root.go index 8644bed4b..68bf56b05 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,7 +9,9 @@ import ( "github.com/anchore/imgbom/imgbom/presenter" "github.com/anchore/imgbom/internal" "github.com/anchore/imgbom/internal/bus" + "github.com/anchore/imgbom/internal/log" "github.com/anchore/imgbom/internal/ui" + "github.com/anchore/imgbom/internal/version" "github.com/spf13/cobra" "github.com/wagoodman/go-partybus" ) @@ -26,9 +28,13 @@ Supports the following image sources: `, map[string]interface{}{ "appName": internal.ApplicationName, }), - Args: cobra.MaximumNArgs(1), + Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - os.Exit(doRunCmd(cmd, args)) + err := doRunCmd(cmd, args) + if err != nil { + log.Errorf(err.Error()) + os.Exit(1) + } }, } @@ -37,6 +43,21 @@ func startWorker(userInput string) <-chan error { go func() { defer close(errs) + if appConfig.CheckForAppUpdate { + isAvailable, newVersion, err := version.IsUpdateAvailable() + if err != nil { + log.Errorf(err.Error()) + } + if isAvailable { + log.Infof("New version of %s is available: %s", internal.ApplicationName, newVersion) + + bus.Publish(partybus.Event{ + Type: event.AppUpdateAvailable, + Value: newVersion, + }) + } + } + catalog, scope, _, err := imgbom.Catalog(userInput, appConfig.ScopeOpt) if err != nil { errs <- fmt.Errorf("failed to catalog input: %+v", err) @@ -51,10 +72,9 @@ func startWorker(userInput string) <-chan error { return errs } -func doRunCmd(_ *cobra.Command, args []string) int { - errs := startWorker(args[0]) - +func doRunCmd(_ *cobra.Command, args []string) error { + userInput := args[0] + errs := startWorker(userInput) ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet) - return ux(errs, eventSubscription) } diff --git a/cmd/version.go b/cmd/version.go index e96dc179a..01f434ed8 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -4,16 +4,11 @@ import ( "fmt" "github.com/anchore/imgbom/internal" + "github.com/anchore/imgbom/internal/version" "github.com/spf13/cobra" ) -type Version struct { - Version string - Commit string - BuildTime string -} - -var version *Version +var showVerboseVersionInfo bool var versionCmd = &cobra.Command{ Use: "version", @@ -22,13 +17,23 @@ var versionCmd = &cobra.Command{ } func init() { + versionCmd.Flags().BoolVarP(&showVerboseVersionInfo, "verbose", "v", false, "show additional version information") + rootCmd.AddCommand(versionCmd) } -func SetVersion(v *Version) { - version = v -} - -func printVersion(cmd *cobra.Command, args []string) { - fmt.Printf("%s %s\n", internal.ApplicationName, version.Version) +func printVersion(_ *cobra.Command, _ []string) { + versionInfo := version.FromBuild() + if showVerboseVersionInfo { + fmt.Println("Application: ", internal.ApplicationName) + fmt.Println("Version: ", versionInfo.Version) + fmt.Println("BuildDate: ", versionInfo.BuildDate) + fmt.Println("GitCommit: ", versionInfo.GitCommit) + fmt.Println("GitTreeState: ", versionInfo.GitTreeState) + fmt.Println("Platform: ", versionInfo.Platform) + fmt.Println("GoVersion: ", versionInfo.GoVersion) + fmt.Println("Compiler: ", versionInfo.Compiler) + } else { + fmt.Printf("%s %s\n", internal.ApplicationName, versionInfo.Version) + } } diff --git a/go.mod b/go.mod index 14df9a9ae..886a74370 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.14 require ( github.com/adrg/xdg v0.2.1 github.com/anchore/go-testutils v0.0.0-20200624184116-66aa578126db + github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/stereoscope v0.0.0-20200706164556-7cf39d7f4639 github.com/bmatcuk/doublestar v1.3.1 github.com/go-test/deep v1.0.6 diff --git a/go.sum b/go.sum index 0cc7ff12e..5726dea60 100644 --- a/go.sum +++ b/go.sum @@ -124,13 +124,11 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/anchore/go-testutils v0.0.0-20200520222037-edc2bf1864fe h1:YMXe4RA3qy4Ri5fmGQii/Gn+Pxv3oBfiS/LqzeOVuwo= -github.com/anchore/go-testutils v0.0.0-20200520222037-edc2bf1864fe/go.mod h1:D3rc2L/q4Hcp9eeX6AIJH4Q+kPjOtJCFhG9za90j+nU= github.com/anchore/go-testutils v0.0.0-20200624184116-66aa578126db h1:LWKezJnFTFxNkZ4MzajVf+YWvJS0+7hwFr59u6SS7cw= github.com/anchore/go-testutils v0.0.0-20200624184116-66aa578126db/go.mod h1:D3rc2L/q4Hcp9eeX6AIJH4Q+kPjOtJCFhG9za90j+nU= +github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods= +github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/stereoscope v0.0.0-20200520221116-025e07f1c93e/go.mod h1:bkyLl5VITnrmgErv4S1vDfVz/TGAZ5il6161IQo7w2g= -github.com/anchore/stereoscope v0.0.0-20200624175800-ef5dbfb7cae4 h1:bPd6YFo9VDyoTLVcawFNbW9Z8dQA3M/pCgdD22dR0VQ= -github.com/anchore/stereoscope v0.0.0-20200624175800-ef5dbfb7cae4/go.mod h1:f4LZpPnN/5RpQnzcznDsYNeYavFCAW8CpbHN01G3Lh8= github.com/anchore/stereoscope v0.0.0-20200706164556-7cf39d7f4639 h1:J1oytkj+aBuACNF2whtEiVxRXIZ8zwT+EiPTqm/FvwA= github.com/anchore/stereoscope v0.0.0-20200706164556-7cf39d7f4639/go.mod h1:WntReQTI/I27FOQ87UgLVVzWgku6+ZsqfOTLxpIZFCs= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= @@ -408,6 +406,7 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -520,6 +519,7 @@ github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgb github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -612,9 +612,11 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= @@ -1200,6 +1202,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.56.0 h1:DPMeDvGTM54DXbPkVIZsp19fp/I2K7zwA/itHYHKo8Y= @@ -1233,8 +1236,10 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.17.4/go.mod h1:5qxx6vjmwUVG2nHQTKGlLts8Tbok8PzHl4vHtVFuZCA= +k8s.io/apimachinery v0.17.4 h1:UzM+38cPUJnzqSQ+E1PY4YxMHIzQyCg29LOoGfo79Zw= k8s.io/apimachinery v0.17.4/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= k8s.io/apiserver v0.17.4/go.mod h1:5ZDQ6Xr5MNBxyi3iUZXS84QOhZl+W7Oq2us/29c0j9I= +k8s.io/client-go v0.17.4 h1:VVdVbpTY70jiNHS1eiFkUt7ZIJX3txd29nDxxXH4en8= k8s.io/client-go v0.17.4/go.mod h1:ouF6o5pz3is8qU0/qYL2RnoxOPqgfuidYLowytyLJmc= k8s.io/cloud-provider v0.17.4/go.mod h1:XEjKDzfD+b9MTLXQFlDGkk6Ho8SGMpaU8Uugx/KNK9U= k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= @@ -1244,6 +1249,7 @@ k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8 k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/legacy-cloud-providers v0.17.4/go.mod h1:FikRNoD64ECjkxO36gkDgJeiQWwyZTuBkhu+yxOc1Js= @@ -1266,6 +1272,7 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= sourcegraph.com/sqs/pbtypes v1.0.0 h1:f7lAwqviDEGvON4kRv0o5V7FT/IQK+tbkF664XMbP3o= diff --git a/imgbom/event/event.go b/imgbom/event/event.go index 910eaa806..ea573635a 100644 --- a/imgbom/event/event.go +++ b/imgbom/event/event.go @@ -3,6 +3,7 @@ package event import "github.com/wagoodman/go-partybus" const ( - CatalogerStarted partybus.EventType = "cataloger-started-event" - CatalogerFinished partybus.EventType = "cataloger-finished-event" + AppUpdateAvailable partybus.EventType = "app-update-available" + CatalogerStarted partybus.EventType = "cataloger-started-event" + CatalogerFinished partybus.EventType = "cataloger-finished-event" ) diff --git a/imgbom/event/parsers/parsers.go b/imgbom/event/parsers/parsers.go index 6795c3a47..6ae79ad5b 100644 --- a/imgbom/event/parsers/parsers.go +++ b/imgbom/event/parsers/parsers.go @@ -59,3 +59,16 @@ func ParseCatalogerFinished(e partybus.Event) (presenter.Presenter, error) { return pres, nil } + +func ParseAppUpdateAvailable(e partybus.Event) (string, error) { + if err := checkEventType(e.Type, event.AppUpdateAvailable); err != nil { + return "", err + } + + newVersion, ok := e.Value.(string) + if !ok { + return "", newPayloadErr(e.Type, "Value", e.Value) + } + + return newVersion, nil +} diff --git a/imgbom/lib.go b/imgbom/lib.go index 2b4d2ea84..6b7843ca0 100644 --- a/imgbom/lib.go +++ b/imgbom/lib.go @@ -1,8 +1,6 @@ package imgbom import ( - "fmt" - "github.com/anchore/imgbom/imgbom/cataloger" "github.com/anchore/imgbom/imgbom/distro" "github.com/anchore/imgbom/imgbom/logger" @@ -17,14 +15,14 @@ func Catalog(userInput string, scoptOpt scope.Option) (*pkg.Catalog, *scope.Scop s, cleanup, err := scope.NewScope(userInput, scoptOpt) defer cleanup() if err != nil { - return nil, nil, nil, fmt.Errorf("failed to create scope: %w", err) + return nil, nil, nil, err } d := IdentifyDistro(s) catalog, err := CatalogFromScope(s) if err != nil { - return nil, nil, nil, fmt.Errorf("failed to produce catalog: %w", err) + return nil, nil, nil, err } return catalog, &s, &d, nil diff --git a/internal/config/config.go b/internal/config/config.go index abb10ebeb..a7b69edea 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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) { diff --git a/internal/ui/etui/ephemeral_tui.go b/internal/ui/etui/ephemeral_tui.go index b769c5495..14124f30f 100644 --- a/internal/ui/etui/ephemeral_tui.go +++ b/internal/ui/etui/ephemeral_tui.go @@ -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 } diff --git a/internal/ui/etui/event_handlers.go b/internal/ui/etui/event_handlers.go index 449a39c14..61538f9e5 100644 --- a/internal/ui/etui/event_handlers.go +++ b/internal/ui/etui/event_handlers.go @@ -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 +} diff --git a/internal/ui/logger_output.go b/internal/ui/logger_output.go index ab3bc2462..51f5081a6 100644 --- a/internal/ui/logger_output.go +++ b/internal/ui/logger_output.go @@ -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 } diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 855e7e32d..dd06cadd0 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -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 diff --git a/internal/version/build.go b/internal/version/build.go new file mode 100644 index 000000000..bd3e2dd35 --- /dev/null +++ b/internal/version/build.go @@ -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, + } +} diff --git a/internal/version/update.go b/internal/version/update.go new file mode 100644 index 000000000..421ca228c --- /dev/null +++ b/internal/version/update.go @@ -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) +} diff --git a/internal/version/update_test.go b/internal/version/update_test.go new file mode 100644 index 000000000..2484d69f6 --- /dev/null +++ b/internal/version/update_test.go @@ -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()) + } + }) + } + +} diff --git a/main.go b/main.go index 4529faf7d..f3e8a8004 100644 --- a/main.go +++ b/main.go @@ -4,18 +4,6 @@ import ( "github.com/anchore/imgbom/cmd" ) -var ( - version = "No version provided" - commit = "No commit provided" - buildTime = "No build timestamp provided" -) - func main() { - cmd.SetVersion(&cmd.Version{ - Version: version, - Commit: commit, - BuildTime: buildTime, - }) - cmd.Execute() }