Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 37 additions & 21 deletions cli/cmd/mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions cli/cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
package cmd

import (
"fmt"
"log"

"github.com/spf13/cobra"
)
Expand All @@ -14,7 +14,7 @@ type UpdateCmd struct {
}

func (c *UpdateCmd) RunE(_ *cobra.Command, args []string) error {
fmt.Printf("running %s", c.cmd.Use)
log.Printf("running %s", c.cmd.Use)

return nil
}
Expand Down
3 changes: 2 additions & 1 deletion cli/cmd/update_api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package cmd

import (
"fmt"
"log"
"time"

"github.com/codesphere-cloud/oms/internal/portal"
Expand Down Expand Up @@ -58,6 +59,6 @@ func (c *UpdateAPIKeyCmd) UpdateAPIKey(p portal.Portal) error {
return fmt.Errorf("failed to update API key: %w", err)
}

fmt.Printf("Successfully updated API key '%s' with new expiration date %s.\n", c.Opts.APIKeyID, expiresAt.Format(time.RFC1123))
log.Printf("Successfully updated API key '%s' with new expiration date %s.\n", c.Opts.APIKeyID, expiresAt.Format(time.RFC1123))
return nil
}
83 changes: 22 additions & 61 deletions cli/cmd/update_oms.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,30 @@ package cmd

import (
"fmt"
"io"
"strings"
"log"

"github.com/blang/semver"
"github.com/inconshreveable/go-update"
"github.com/rhysd/go-github-selfupdate/selfupdate"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"

"github.com/codesphere-cloud/oms/internal/portal"
"github.com/codesphere-cloud/oms/internal/util"
"github.com/codesphere-cloud/oms/internal/version"
)

const GitHubRepo = "codesphere-cloud/oms"

type OMSUpdater interface {
Apply(update io.Reader) error
Update(v semver.Version, repo string) (semver.Version, string, error)
}

type OMSSelfUpdater struct{}

func (s *OMSSelfUpdater) Apply(r io.Reader) error {
return update.Apply(r, update.Options{})
func (s *OMSSelfUpdater) Update(v semver.Version, repo string) (semver.Version, string, error) {
latest, err := selfupdate.UpdateSelf(v, repo)
if err != nil {
return v, "", err
}

return latest.Version, latest.ReleaseNotes, nil
}

type UpdateOmsCmd struct {
Expand All @@ -42,69 +45,27 @@ func AddOmsUpdateCmd(parentCmd *cobra.Command) {
omsCmd := &cobra.Command{
Use: "oms",
Short: "Update the OMS CLI",
Long: `Updates the OMS CLI to the latest release from OMS Portal.`,
Long: `Updates the OMS CLI to the latest release from GitHub.`,
RunE: func(_ *cobra.Command, args []string) error {
p := portal.NewPortalClient()
return cmdState.SelfUpdate(p)
return cmdState.SelfUpdate()
},
}
parentCmd.AddCommand(omsCmd)
}

func (c *UpdateOmsCmd) SelfUpdate(p portal.Portal) error {
currentVersion := semver.MustParse(c.Version.Version())

latest, err := p.GetBuild(portal.OmsProduct, "", "")
func (c *UpdateOmsCmd) SelfUpdate() error {
v := semver.MustParse(c.Version.Version())
latestVersion, releaseNotes, err := c.Updater.Update(v, GitHubRepo)
if err != nil {
return fmt.Errorf("failed to query OMS Portal for latest version: %w", err)
return fmt.Errorf("update failed: %w", err)
}
latestVersion := semver.MustParse(strings.TrimPrefix(latest.Version, "oms-v"))

fmt.Printf("current version: %v\n", currentVersion)
fmt.Printf("latest version: %v\n", latestVersion)
if latestVersion.Equals(currentVersion) {
fmt.Println("Current OMS CLI is already the latest version", c.Version.Version())
if latestVersion.Equals(v) {
log.Println("Current OMS CLI is the latest version", c.Version.Version())
return nil
}

// Need a build with a single artifact to download it
download, err := latest.GetBuildForDownload(fmt.Sprintf("%s_%s.tar.gz", c.Version.Os(), c.Version.Arch()))
if err != nil {
return fmt.Errorf("failed to find OMS CLI in package: %w", err)
}

// Use a pipe to unzip the file while downloading without storing on the filesystem
reader, writer := io.Pipe()
defer func() { _ = reader.Close() }()

eg := errgroup.Group{}
eg.Go(func() error {
defer func() { _ = writer.Close() }()
err = p.DownloadBuildArtifact(portal.OmsProduct, download, writer)
if err != nil {
return fmt.Errorf("failed to download latest OMS package: %w", err)
}
return nil
})

cliReader, err := util.StreamFileFromGzip(reader, "oms-cli")
if err != nil {
return fmt.Errorf("failed to extract binary from archive: %w", err)
}

err = c.Updater.Apply(cliReader)
if err != nil {
return fmt.Errorf("failed to apply update: %w", err)
}

_, _ = io.Copy(io.Discard, reader)

// Wait for download to finish and handle any error from the go routine
err = eg.Wait()
if err != nil {
return err
}
log.Printf("Successfully updated from %s to %s\n", v.String(), latestVersion.String())
log.Println("Release notes:\n", releaseNotes)

fmt.Println("Update finished successfully.")
return nil
}
98 changes: 28 additions & 70 deletions cli/cmd/update_oms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,98 +4,56 @@
package cmd_test

import (
"embed"
"io"
"github.com/blang/semver"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"

"github.com/codesphere-cloud/oms/cli/cmd"
"github.com/codesphere-cloud/oms/internal/portal"
"github.com/codesphere-cloud/oms/internal/version"
)

// I didn't find a good way to do this in memory without just reversing the code under test.
// While this is not ideal, at least it doesn't read from file during the test run but during compilation.
//
//go:generate mkdir -p testdata
//go:generate sh -c "echo fake-cli > testdata/oms-cli"
//go:generate sh -c "cd testdata && tar cfz testcli.tar.gz oms-cli"
//go:embed testdata
var testdata embed.FS
type mockOMSUpdater struct{ mock.Mock }

var _ = Describe("Update", func() {
func (m *mockOMSUpdater) Update(v semver.Version, repo string) (semver.Version, string, error) {
args := m.Called(v, repo)
return args.Get(0).(semver.Version), args.String(1), args.Error(2)
}

var _ = Describe("Update", func() {
var (
mockPortal *portal.MockPortal
mockVersion *version.MockVersion
mockUpdater *cmd.MockOMSUpdater
latestBuild portal.Build
buildToDownload portal.Build
c cmd.UpdateOmsCmd
mockVersion *version.MockVersion
mockGit *mockOMSUpdater
c cmd.UpdateOmsCmd
)

BeforeEach(func() {
mockPortal = portal.NewMockPortal(GinkgoT())
mockVersion = version.NewMockVersion(GinkgoT())
mockUpdater = cmd.NewMockOMSUpdater(GinkgoT())

latestBuild = portal.Build{
Version: "0.0.42",
Artifacts: []portal.Artifact{
{Filename: "fakeos_fakearch.tar.gz"},
{Filename: "fakeos2_fakearch2.tar.gz"},
{Filename: "fakeos3_fakearch3.tar.gz"},
},
}
buildToDownload = portal.Build{
Version: "0.0.42",
Artifacts: []portal.Artifact{
{Filename: "fakeos_fakearch.tar.gz"},
},
}
mockGit = &mockOMSUpdater{}
c = cmd.UpdateOmsCmd{
Version: mockVersion,
Updater: mockUpdater,
Updater: mockGit,
}
})

Describe("SelfUpdate", func() {
It("Extracts oms-cli from the downloaded archive", func() {
mockVersion.EXPECT().Arch().Return("fakearch")
mockVersion.EXPECT().Version().Return("0.0.0")
mockVersion.EXPECT().Os().Return("fakeos")
mockPortal.EXPECT().GetBuild(portal.OmsProduct, "", "").Return(latestBuild, nil)
mockPortal.EXPECT().DownloadBuildArtifact(portal.OmsProduct, buildToDownload, mock.Anything).RunAndReturn(
func(product portal.Product, build portal.Build, file io.Writer) error {
embeddedFile, err := testdata.Open("testdata/testcli.tar.gz")
if err != nil {
Expect(err).NotTo(HaveOccurred())
}
defer func() { _ = embeddedFile.Close() }()
It("Detects when current version is latest version", func() {
v := "0.0.42"
mockVersion.EXPECT().Version().Return(v)

if _, err := io.Copy(file, embeddedFile); err != nil {
Expect(err).NotTo(HaveOccurred())
}
return nil
})
mockUpdater.EXPECT().Apply(mock.Anything).RunAndReturn(func(update io.Reader) error {
output, err := io.ReadAll(update)
Expect(err).NotTo(HaveOccurred())
// file content written in go:generate
Expect(string(output)).To(Equal("fake-cli\n"))
return nil
})
err := c.SelfUpdate(mockPortal)
Expect(err).NotTo(HaveOccurred())
})
mockGit.On("Update", semver.MustParse(v), cmd.GitHubRepo).Return(semver.MustParse(v), "", nil)
err := c.SelfUpdate()
Expect(err).NotTo(HaveOccurred())
mockGit.AssertExpectations(GinkgoT())
})

It("Detects when current version is latest version", func() {
mockVersion.EXPECT().Version().Return(latestBuild.Version)
mockPortal.EXPECT().GetBuild(portal.OmsProduct, "", "").Return(latestBuild, nil)
err := c.SelfUpdate(mockPortal)
Expect(err).NotTo(HaveOccurred())
})
It("Updates when a newer version exists", func() {
current := "0.0.0"
latest := "0.0.42"
mockVersion.EXPECT().Version().Return(current)
mockGit.On("Update", semver.MustParse(current), cmd.GitHubRepo).Return(semver.MustParse(latest), "notes", nil)
err := c.SelfUpdate()
Expect(err).NotTo(HaveOccurred())
mockGit.AssertExpectations(GinkgoT())
})
})
Loading