Skip to content

Commit

Permalink
Merge pull request #2309 from neogopher/fix-upgrade-command
Browse files Browse the repository at this point in the history
bugfix(cli): change process of finding specified binary releases
  • Loading branch information
ThomasK33 authored Dec 4, 2024
2 parents e375813 + ed3c336 commit d636659
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 7 deletions.
10 changes: 7 additions & 3 deletions cmd/vclusterctl/cmd/upgrade.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"context"

"github.com/loft-sh/log"
"github.com/loft-sh/vcluster/pkg/upgrade"
"github.com/pkg/errors"
Expand Down Expand Up @@ -28,16 +30,18 @@ func NewUpgradeCmd() *cobra.Command {
Upgrades the vcluster CLI to the newest version
#######################################################`,
Args: cobra.NoArgs,
RunE: cmd.Run,
RunE: func(cobraCmd *cobra.Command, _ []string) error {
return cmd.Run(cobraCmd.Context())
},
}

upgradeCmd.Flags().StringVar(&cmd.Version, "version", "", "The version to update vcluster to. Defaults to the latest stable version available")
return upgradeCmd
}

// Run executes the command logic
func (cmd *UpgradeCmd) Run(*cobra.Command, []string) error {
err := upgrade.Upgrade(cmd.Version, cmd.log)
func (cmd *UpgradeCmd) Run(ctx context.Context) error {
err := upgrade.Upgrade(ctx, cmd.Version, cmd.log)
if err != nil {
return errors.Errorf("Couldn't upgrade: %v", err)
}
Expand Down
105 changes: 105 additions & 0 deletions pkg/upgrade/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package upgrade

import (
"context"
"fmt"
"net/http"
"os"
"regexp"
"runtime"
"strings"

"github.com/blang/semver"
"github.com/google/go-github/v30/github"
"github.com/pkg/errors"
gitconfig "github.com/tcnksm/go-gitconfig"
"golang.org/x/oauth2"
)

func fetchReleaseByTag(ctx context.Context, owner, repo, tag string) (*github.RepositoryRelease, error) {
var (
token string
hc *http.Client

release *github.RepositoryRelease
)

if os.Getenv("GITHUB_TOKEN") != "" {
token = os.Getenv("GITHUB_TOKEN")
}
if token == "" {
token, _ = gitconfig.GithubToken()
}

if token == "" {
hc = http.DefaultClient
} else {
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
hc = oauth2.NewClient(ctx, src)
}

client := github.NewClient(hc)

// Fetch the release by tag
release, _, err := client.Repositories.GetReleaseByTag(ctx, owner, repo, tag)
if err != nil {
return release, fmt.Errorf("error fetching release by tag: %w", err)
}

return release, nil
}

func findAssetFromRelease(rel *github.RepositoryRelease) (*github.ReleaseAsset, semver.Version, bool, error) {
// Generate candidates
suffixes := make([]string, 0, 2*7*2)
for _, sep := range []rune{'_', '-'} {
for _, ext := range []string{".zip", ".tar.gz", ".tgz", ".gzip", ".gz", ".tar.xz", ".xz", ""} {
suffix := fmt.Sprintf("%s%c%s%s", runtime.GOOS, sep, runtime.GOARCH, ext)
suffixes = append(suffixes, suffix)
if runtime.GOOS == "windows" {
suffix = fmt.Sprintf("%s%c%s.exe%s", runtime.GOOS, sep, runtime.GOARCH, ext)
suffixes = append(suffixes, suffix)
}
}
}

verText := rel.GetTagName()
indices := reVersion.FindStringIndex(verText)
if indices == nil {
return nil, semver.Version{}, false, fmt.Errorf("skip version not adopting semver %s", verText)
}
if indices[0] > 0 {
// Strip prefix of version
verText = verText[indices[0]:]
}

// If semver cannot parse the version text, it means that the text is not adopting
// the semantic versioning. So it should be skipped.
ver, err := semver.Make(verText)
if err != nil {
return nil, semver.Version{}, false, fmt.Errorf("failed to parse a semantic version %s", verText)
}

filterRe, err := regexp.Compile("vcluster")
if err != nil {
return nil, semver.Version{}, false, errors.New("failed to compile regexp")
}

for _, asset := range rel.Assets {
name := asset.GetName()

// Skipping asset not matching filter
if !filterRe.MatchString(name) {
continue
}

for _, s := range suffixes {
if strings.HasSuffix(name, s) { // require version, arch etc
// default: assume single artifact
return asset, ver, true, nil
}
}
}

return nil, semver.Version{}, false, fmt.Errorf("no suitable asset was found in release %s", rel.GetTagName())
}
46 changes: 44 additions & 2 deletions pkg/upgrade/upgrade.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package upgrade

import (
"context"
"fmt"
"os"
"regexp"
"strings"
"sync"

"github.com/loft-sh/log"
Expand Down Expand Up @@ -115,15 +117,15 @@ func NewerVersionAvailable() string {
}

// Upgrade downloads the latest release from github and replaces vcluster if a new version is found
func Upgrade(flagVersion string, log log.Logger) error {
func Upgrade(ctx context.Context, flagVersion string, log log.Logger) error {
updater, err := selfupdate.NewUpdater(selfupdate.Config{
Filters: []string{"vcluster"},
})
if err != nil {
return fmt.Errorf("failed to initialize updater: %w", err)
}
if flagVersion != "" {
release, found, err := updater.DetectVersion(githubSlug, flagVersion)
release, found, err := DetectVersion(ctx, githubSlug, flagVersion)
if err != nil {
return errors.Wrap(err, "find version")
} else if !found {
Expand Down Expand Up @@ -186,3 +188,43 @@ func Upgrade(flagVersion string, log log.Logger) error {

return nil
}

func DetectVersion(ctx context.Context, slug string, version string) (*selfupdate.Release, bool, error) {
var (
release *selfupdate.Release
found bool
err error
)

repo := strings.Split(slug, "/")
if len(repo) != 2 || repo[0] == "" || repo[1] == "" {
return nil, false, fmt.Errorf("invalid slug format. It should be 'owner/name': %s", slug)
}

githubRelease, err := fetchReleaseByTag(ctx, repo[0], repo[1], version)
if err != nil {
return nil, false, fmt.Errorf("repository or release not found: %w", err)
}

asset, semVer, found, err := findAssetFromRelease(githubRelease)
if !found {
return nil, false, fmt.Errorf("release asset not found: %w", err)
}

publishedAt := githubRelease.GetPublishedAt().Time
release = &selfupdate.Release{
Version: semVer,
AssetURL: asset.GetBrowserDownloadURL(),
AssetByteSize: asset.GetSize(),
AssetID: asset.GetID(),
ValidationAssetID: -1,
URL: githubRelease.GetHTMLURL(),
ReleaseNotes: githubRelease.GetBody(),
Name: githubRelease.GetName(),
PublishedAt: &publishedAt,
RepoOwner: repo[0],
RepoName: repo[1],
}

return release, true, nil
}
6 changes: 4 additions & 2 deletions pkg/upgrade/upgrade_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package upgrade

import (
"context"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -75,7 +76,8 @@ func TestUpgrade(t *testing.T) {
defer func() { version = versionBackup }()

// Newest version already reached
err = Upgrade("", log.GetInstance())
ctx := context.Background()
err = Upgrade(ctx, "", log.GetInstance())
assert.Equal(t, false, err != nil, "Upgrade returned error if newest version already reached")
err = logFile.Close()
if err != nil {
Expand All @@ -91,6 +93,6 @@ func TestUpgrade(t *testing.T) {
githubSlugBackup := githubSlug
githubSlug = ""
defer func() { githubSlug = githubSlugBackup }()
err = Upgrade("", log.GetInstance())
err = Upgrade(ctx, "", log.GetInstance())
assert.Equal(t, true, err != nil, "No error returned if DetectLatest returns one.")
}

0 comments on commit d636659

Please sign in to comment.