Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.

86 changes: 21 additions & 65 deletions cli/cmd/update_oms.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,25 @@ package cmd

import (
"fmt"
"io"
"strings"

"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"
)

type OMSUpdater interface {
Apply(update io.Reader) error
}
const GitHubRepo = "codesphere-cloud/oms"

type OMSUpdater func(v semver.Version, repo string) (semver.Version, string, error)

type OMSSelfUpdater struct{}
var OMSSelfUpdater OMSUpdater = func(v semver.Version, repo string) (semver.Version, string, error) {
latest, err := selfupdate.UpdateSelf(v, repo)
if err != nil {
return v, "", err
}

func (s *OMSSelfUpdater) Apply(r io.Reader) error {
return update.Apply(r, update.Options{})
return latest.Version, latest.ReleaseNotes, nil
}

type UpdateOmsCmd struct {
Expand All @@ -36,75 +34,33 @@ type UpdateOmsCmd struct {
func AddOmsUpdateCmd(parentCmd *cobra.Command) {
cmdState := &UpdateOmsCmd{
Version: &version.Build{},
Updater: &OMSSelfUpdater{},
Updater: OMSSelfUpdater,
}

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(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) {
fmt.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
}
fmt.Printf("Successfully updated from %s to %s\n", v.String(), latestVersion.String())
fmt.Println("Release notes:\n", releaseNotes)

fmt.Println("Update finished successfully.")
return nil
}
100 changes: 31 additions & 69 deletions cli/cmd/update_oms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,98 +4,60 @@
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{}
// GitUpdate is a function type; forward calls to the testify mock.
gitFunc := func(v semver.Version, repo string) (semver.Version, string, error) {
return mockGit.Update(v, repo)
}
c = cmd.UpdateOmsCmd{
Version: mockVersion,
Updater: mockUpdater,
Updater: gitFunc,
}
})

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())
})
})
10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.24.2

require (
github.com/blang/semver v3.5.1+incompatible
github.com/codesphere-cloud/cs-go v0.11.1
github.com/codesphere-cloud/cs-go v0.6.1
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf
github.com/jedib0t/go-pretty/v6 v6.6.8
github.com/onsi/ginkgo/v2 v2.25.3
Expand All @@ -22,25 +22,33 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/golang/protobuf v1.2.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rhysd/go-github-selfupdate v1.2.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
github.com/ulikunitz/xz v0.5.5 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/appengine v1.3.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading