From 229ef32bcbcce794ef91edf40bcefa27c436af1e Mon Sep 17 00:00:00 2001 From: Sid Kattoju Date: Thu, 7 Dec 2023 11:09:16 -0500 Subject: [PATCH] add check for latest available version Signed-off-by: Sid Kattoju --- Makefile | 4 +- cmd/preflight/cmd/check_container.go | 59 ++++++++-- cmd/preflight/cmd/check_container_test.go | 24 ++--- go.mod | 3 + go.sum | 7 ++ version/version.go | 44 +++++++- version/version_test.go | 126 +++++++++++++++++++++- 7 files changed, 241 insertions(+), 26 deletions(-) diff --git a/Makefile b/Makefile index 755642473..36a657bc0 100644 --- a/Makefile +++ b/Makefile @@ -47,12 +47,12 @@ image-push: .PHONY: test test: go test -v $$(go list ./... | grep -v e2e) \ - -ldflags "-X github.com/redhat-openshift-ecosystem/openshift-preflight/version.commit=bar -X github.com/redhat-openshift-ecosystem/openshift-preflight/version.version=foo" + -ldflags "-X github.com/redhat-openshift-ecosystem/openshift-preflight/version.commit=foobar -X github.com/redhat-openshift-ecosystem/openshift-preflight/version.version=0.0.1" .PHONY: cover cover: go test -v \ - -ldflags "-X github.com/redhat-openshift-ecosystem/openshift-preflight/version.commit=bar -X github.com/redhat-openshift-ecosystem/openshift-preflight/version.version=foo" \ + -ldflags "-X github.com/redhat-openshift-ecosystem/openshift-preflight/version.commit=foobar -X github.com/redhat-openshift-ecosystem/openshift-preflight/version.version=0.0.1" \ $$(go list ./... | grep -v e2e) \ -race \ -cover -coverprofile=coverage.out diff --git a/cmd/preflight/cmd/check_container.go b/cmd/preflight/cmd/check_container.go index 2283178ff..38defa942 100644 --- a/cmd/preflight/cmd/check_container.go +++ b/cmd/preflight/cmd/check_container.go @@ -6,10 +6,20 @@ import ( "context" "fmt" "io" + "net/http" "os" "path/filepath" rt "runtime" "strings" + "time" + + "github.com/go-logr/logr" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-github/v57/github" + "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/redhat-openshift-ecosystem/openshift-preflight/artifacts" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification" @@ -23,13 +33,6 @@ import ( "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/runtime" "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/viper" "github.com/redhat-openshift-ecosystem/openshift-preflight/version" - - "github.com/go-logr/logr" - "github.com/google/go-containerregistry/pkg/crane" - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/spf13/cobra" - "github.com/spf13/pflag" ) var submit bool @@ -45,7 +48,7 @@ func checkContainerCmd(runpreflight runPreflight) *cobra.Command { Args: checkContainerPositionalArgs, // this fmt.Sprintf is in place to keep spacing consistent with cobras two spaces that's used in: Usage, Flags, etc Example: fmt.Sprintf(" %s", "preflight check container quay.io/repo-name/container-name:version"), - PreRunE: validateCertificationProjectID, + PreRunE: validateConditions, RunE: func(cmd *cobra.Command, args []string) error { return checkContainerRunE(cmd, args, runpreflight) }, @@ -88,6 +91,9 @@ func checkContainerCmd(runpreflight runPreflight) *cobra.Command { flags.String("platform", rt.GOARCH, "Architecture of image to pull. Defaults to runtime platform.") _ = viper.BindPFlag("platform", flags.Lookup("platform")) + flags.String("gh-auth-token", "", "A Github auth token can be specified to work around rate limits") + _ = viper.BindPFlag("gh-auth-token", flags.Lookup("gh-auth-token")) + return checkContainerCmd } @@ -237,9 +243,16 @@ func checkContainerPositionalArgs(cmd *cobra.Command, args []string) error { return nil } +// validateConditions run all pre-run functions +func validateConditions(cmd *cobra.Command, args []string) error { + err := validateCertificationProjectID() + checkForNewerReleaseVersion(cmd) + return err +} + // validateCertificationProjectID validates that the certification project id is in the proper format // and throws an error if the value provided is in a legacy format that is not usable to query pyxis -func validateCertificationProjectID(cmd *cobra.Command, args []string) error { +func validateCertificationProjectID() error { viper := viper.Instance() certificationProjectID := viper.GetString("certification_project_id") // splitting the certification project id into parts. if there are more than 2 elements in the array, @@ -257,6 +270,34 @@ func validateCertificationProjectID(cmd *cobra.Command, args []string) error { return nil } +// checkForNewerReleaseVersion checks if there is a newer release available +func checkForNewerReleaseVersion(cmd *cobra.Command) { + logger := logr.FromContextOrDiscard(cmd.Context()) + + // use an authenticated client if a token is provided + var client *github.Client + ghToken, err := cmd.Flags().GetString("gh-auth-token") + if err == nil && len(ghToken) > 0 { + client = github.NewClient(&http.Client{ + // Timeout in 1s in case Github is slow to respond + Timeout: time.Second * 1, + }).WithAuthToken(ghToken) + } else { + client = github.NewClient(&http.Client{ + // timeout in 1s in case Github is slow to respond + Timeout: time.Second * 1, + }) + } + // check if a newer release is available + latestRelease, err := version.Version.LatestReleasedVersion(cmd, client.Repositories) + if err != nil { + logger.Error(err, "Unable to determine if running the latest release") + } + if latestRelease != nil { + logger.Info("Found newer release", "New version", *latestRelease.TagName, "available at", *latestRelease.HTMLURL) + } +} + // generateContainerCheckOptions returns appropriate container.Options based on cfg. func generateContainerCheckOptions(cfg *runtime.Config) []container.Option { o := []container.Option{ diff --git a/cmd/preflight/cmd/check_container_test.go b/cmd/preflight/cmd/check_container_test.go index 54f799663..22c5e4165 100644 --- a/cmd/preflight/cmd/check_container_test.go +++ b/cmd/preflight/cmd/check_container_test.go @@ -13,14 +13,6 @@ import ( "path/filepath" "runtime" - "github.com/redhat-openshift-ecosystem/openshift-preflight/artifacts" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification" - "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/check" - "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/cli" - "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/formatters" - "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/lib" - "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/viper" - "github.com/go-logr/logr" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/name" @@ -33,6 +25,14 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/types" + + "github.com/redhat-openshift-ecosystem/openshift-preflight/artifacts" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification" + "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/check" + "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/cli" + "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/formatters" + "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/lib" + "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/viper" ) func createPlatformImage(arch string, addlLayers int) cranev1.Image { @@ -225,7 +225,7 @@ certification_project_id: mycertid` DeferCleanup(viper.Instance().Set, "certification_project_id", "") }) It("should not change the flag value", func() { - err := validateCertificationProjectID(checkContainerCmd(mockRunPreflightReturnNil), []string{"foo"}) + err := validateCertificationProjectID() Expect(err).ToNot(HaveOccurred()) Expect(viper.Instance().GetString("certification_project_id")).To(Equal("123456789")) }) @@ -236,7 +236,7 @@ certification_project_id: mycertid` DeferCleanup(viper.Instance().Set, "certification_project_id", "") }) It("should strip ospid- from the flag value", func() { - err := validateCertificationProjectID(checkContainerCmd(mockRunPreflightReturnNil), []string{"foo"}) + err := validateCertificationProjectID() Expect(err).ToNot(HaveOccurred()) Expect(viper.Instance().GetString("certification_project_id")).To(Equal("123456789")) }) @@ -247,7 +247,7 @@ certification_project_id: mycertid` DeferCleanup(viper.Instance().Set, "certification_project_id", "") }) It("should throw an error", func() { - err := validateCertificationProjectID(checkContainerCmd(mockRunPreflightReturnNil), []string{"foo"}) + err := validateCertificationProjectID() Expect(err).To(HaveOccurred()) }) }) @@ -257,7 +257,7 @@ certification_project_id: mycertid` DeferCleanup(viper.Instance().Set, "certification_project_id", "") }) It("should throw an error", func() { - err := validateCertificationProjectID(checkContainerCmd(mockRunPreflightReturnNil), []string{"foo"}) + err := validateCertificationProjectID() Expect(err).To(HaveOccurred()) }) }) diff --git a/go.mod b/go.mod index 109c1dc50..700b34dfb 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,14 @@ module github.com/redhat-openshift-ecosystem/openshift-preflight go 1.21 require ( + github.com/Masterminds/semver/v3 v3.2.1 github.com/blang/semver v3.5.1+incompatible github.com/bombsimon/logrusr/v4 v4.1.0 github.com/docker/cli v24.0.7+incompatible github.com/glebarez/go-sqlite v1.22.0 github.com/go-logr/logr v1.4.1 github.com/google/go-containerregistry v0.17.0 + github.com/google/go-github/v57 v57.0.0 github.com/knqyf263/go-rpmdb v0.0.0-20230517124904-b97c85e63254 github.com/onsi/ginkgo/v2 v2.13.2 github.com/onsi/gomega v1.30.0 @@ -59,6 +61,7 @@ require ( github.com/google/cel-go v0.16.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect github.com/google/uuid v1.5.0 // indirect diff --git a/go.sum b/go.sum index 2d5e02e53..37b40ffee 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= @@ -79,12 +81,17 @@ github.com/google/cel-go v0.16.1 h1:3hZfSNiAU3KOiNtxuFXVp5WFy4hf/Ly3Sa4/7F8SXNo= github.com/google/cel-go v0.16.1/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk= github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ= +github.com/google/go-github/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q9u7zHs= +github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/version/version.go b/version/version.go index 157ea917e..c0a9e1eee 100644 --- a/version/version.go +++ b/version/version.go @@ -2,7 +2,16 @@ // describing the preflight project. package version -import "fmt" +import ( + "context" + "fmt" + "strings" + + semver "github.com/Masterminds/semver/v3" + "github.com/go-logr/logr" + "github.com/google/go-github/v57/github" + "github.com/spf13/cobra" +) var ( projectName = "github.com/redhat-openshift-ecosystem/openshift-preflight" @@ -16,6 +25,10 @@ var Version = VersionContext{ Commit: commit, } +type VersionClient interface { + GetLatestRelease(ctx context.Context, owner string, repo string) (*github.RepositoryRelease, *github.Response, error) +} + type VersionContext struct { Name string `json:"name"` Version string `json:"version"` @@ -25,3 +38,32 @@ type VersionContext struct { func (vc *VersionContext) String() string { return fmt.Sprintf("%s ", vc.Version, vc.Commit) } + +func (vc *VersionContext) LatestReleasedVersion(cmd *cobra.Command, svc VersionClient) (*github.RepositoryRelease, error) { + ctx := cmd.Context() + logger := logr.FromContextOrDiscard(ctx) + + projectTokens := strings.Split(vc.Name, "/") + owner := projectTokens[1] + repo := projectTokens[2] + // Fetch latest release from Github + latestRelease, resp, err := svc.GetLatestRelease(ctx, owner, repo) + if err != nil { + return nil, err + } + logger.Info("Github responded with", "rate limit", resp.Rate.String()) + currentVersion, err := semver.NewVersion(vc.Version) + if err != nil { + logger.Error(err, "Unable to determine current semver") + return nil, err + } + latestVersion, err := semver.NewVersion(*latestRelease.TagName) + if err != nil { + logger.Error(err, "Unable to determine latest semver") + return nil, err + } + if !currentVersion.Equal(latestVersion) { + return latestRelease, nil + } + return nil, nil +} diff --git a/version/version_test.go b/version/version_test.go index 38eb21ee6..dbc211f82 100644 --- a/version/version_test.go +++ b/version/version_test.go @@ -1,17 +1,26 @@ package version import ( + "context" + "errors" "reflect" "strings" + "github.com/bombsimon/logrusr/v4" + "github.com/go-logr/logr" + "github.com/google/go-github/v57/github" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" + + "github.com/spf13/cobra" ) var _ = Describe("version package utility", func() { // Values assumed to be passed when calling make test. - ldflagVersionOverride := "foo" - ldflagCommitOverride := "bar" + ldflagVersionOverride := "0.0.1" + ldflagCommitOverride := "foobar" // These tests validate that we can override the version and commit information successfully, // and that our string representation includes that information. @@ -51,4 +60,117 @@ var _ = Describe("version package utility", func() { Expect(keys).To(Equal(3)) }) }) + + // These tests validate that GetLatestReleasedVersion fetches the latest available github release + Context("When retrieving latest available release from Github", func() { + Context("When current version is older than the latest version", func() { + It("should return a version", func() { + client := &MockGhVersionClientNewer{} + release, err := Version.LatestReleasedVersion(mockCheckContainerCmd(), client) + Expect(err).To(BeNil()) + Expect(release.TagName) + Expect(release.HTMLURL) + }) + }) + Context("When current version is newer than the latest version", func() { + It("should return nil", func() { + client := &MockGhVersionClientOlder{} + release, err := Version.LatestReleasedVersion(mockCheckContainerCmd(), client) + Expect(err).To(BeNil()) + Expect(release).To(BeNil()) + }) + }) + Context("When the version is not in semver format", func() { + It("should return an error", func() { + client := &MockGhVersionClientBadVersion{} + release, err := Version.LatestReleasedVersion(mockCheckContainerCmd(), client) + Expect(err).To(Not(BeNil())) + Expect(release).To(BeNil()) + }) + }) + Context("When there is an error fetching the latest release from github", func() { + It("should return nil", func() { + client := &MockGhVersionClientError{} + release, err := Version.LatestReleasedVersion(mockCheckContainerCmd(), client) + Expect(err).To(Not(BeNil())) + Expect(release).To(BeNil()) + }) + }) + }) }) + +func mockCheckContainerCmd() *cobra.Command { + mockCheckContainerCmd := cobra.Command{} + mockCheckContainerCmd.SetContext(context.Background()) + logger := logrusr.New(logrus.New()) + ctx := logr.NewContext(mockCheckContainerCmd.Context(), logger) + mockCheckContainerCmd.SetContext(ctx) + flags := mockCheckContainerCmd.Flags() + flags.String("gh-auth-token", "", "A Github auth token can be specified to work around rate limits") + _ = viper.BindPFlag("gh-auth-token", flags.Lookup("gh-auth-token")) + return &mockCheckContainerCmd +} + +type MockGhVersionClientNewer struct{} + +type MockGhVersionClientOlder struct{} + +type MockGhVersionClientError struct{} + +type MockGhVersionClientBadVersion struct{} + +func (mc *MockGhVersionClientNewer) GetLatestRelease(ctx context.Context, owner string, repo string) (*github.RepositoryRelease, *github.Response, error) { + tag := "0.0.2" + url := "test.com/release/0.0.2" + + release := github.RepositoryRelease{ + TagName: &tag, + HTMLURL: &url, + } + response := github.Response{ + Rate: github.Rate{ + Limit: 60, + Remaining: 59, + }, + } + + return &release, &response, nil +} + +func (mc *MockGhVersionClientOlder) GetLatestRelease(ctx context.Context, owner string, repo string) (*github.RepositoryRelease, *github.Response, error) { + tag := "0.0.1" + url := "test.com/release/0.0.1" + release := github.RepositoryRelease{ + TagName: &tag, + HTMLURL: &url, + } + response := github.Response{ + Rate: github.Rate{ + Limit: 60, + Remaining: 59, + }, + } + + return &release, &response, nil +} + +func (mc *MockGhVersionClientBadVersion) GetLatestRelease(ctx context.Context, owner string, repo string) (*github.RepositoryRelease, *github.Response, error) { + tag := "foobar" + url := "test.com/release/foobar" + release := github.RepositoryRelease{ + TagName: &tag, + HTMLURL: &url, + } + response := github.Response{ + Rate: github.Rate{ + Limit: 60, + Remaining: 59, + }, + } + + return &release, &response, nil +} + +func (mc *MockGhVersionClientError) GetLatestRelease(ctx context.Context, owner string, repo string) (*github.RepositoryRelease, *github.Response, error) { + return nil, nil, errors.New("unspecified Error") +}