diff --git a/cmd/vclusterctl/cmd/upgrade.go b/cmd/vclusterctl/cmd/upgrade.go index 2ea886331..50e4f8085 100644 --- a/cmd/vclusterctl/cmd/upgrade.go +++ b/cmd/vclusterctl/cmd/upgrade.go @@ -1,6 +1,8 @@ package cmd import ( + "context" + "github.com/loft-sh/log" "github.com/loft-sh/vcluster/pkg/upgrade" "github.com/pkg/errors" @@ -29,7 +31,9 @@ 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") @@ -37,8 +41,8 @@ Upgrades the vcluster CLI to the newest version } // 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) } diff --git a/pkg/controllers/resources/nodes/syncer.go b/pkg/controllers/resources/nodes/syncer.go index 8e61599f3..938497b8d 100644 --- a/pkg/controllers/resources/nodes/syncer.go +++ b/pkg/controllers/resources/nodes/syncer.go @@ -159,10 +159,10 @@ func (s *nodeSyncer) ModifyController(ctx *synccontext.RegisterContext, bld *bui } // only used when scheduler is enabled -func enqueueNonVclusterPod(old, new client.Object, q workqueue.RateLimitingInterface) { - pod, ok := new.(*corev1.Pod) +func enqueueNonVclusterPod(old, newObj client.Object, q workqueue.RateLimitingInterface) { + pod, ok := newObj.(*corev1.Pod) if !ok { - klog.Errorf("invalid type passed to pod handler: %T", new) + klog.Errorf("invalid type passed to pod handler: %T", newObj) return } // skip if node name missing diff --git a/pkg/helm/time.go b/pkg/helm/time.go index 3eeeeccc0..00acfaf38 100644 --- a/pkg/helm/time.go +++ b/pkg/helm/time.go @@ -49,8 +49,8 @@ func ParseInLocation(layout, value string, loc *time.Location) (Time, error) { return Time{Time: t}, err } -func Date(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) Time { - return Time{Time: time.Date(year, month, day, hour, min, sec, nsec, loc)} +func Date(year int, month time.Month, day, hour, minute, sec, nsec int, loc *time.Location) Time { + return Time{Time: time.Date(year, month, day, hour, minute, sec, nsec, loc)} } func Unix(sec int64, nsec int64) Time { return Time{Time: time.Unix(sec, nsec)} } diff --git a/pkg/upgrade/helpers.go b/pkg/upgrade/helpers.go new file mode 100644 index 000000000..388f13496 --- /dev/null +++ b/pkg/upgrade/helpers.go @@ -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()) +} diff --git a/pkg/upgrade/upgrade.go b/pkg/upgrade/upgrade.go index d2416749e..f929dc1cb 100644 --- a/pkg/upgrade/upgrade.go +++ b/pkg/upgrade/upgrade.go @@ -1,9 +1,11 @@ package upgrade import ( + "context" "fmt" "os" "regexp" + "strings" "sync" "github.com/loft-sh/log" @@ -112,7 +114,7 @@ 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"}, }) @@ -120,7 +122,7 @@ func Upgrade(flagVersion string, log log.Logger) error { 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 { @@ -169,3 +171,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 +} diff --git a/pkg/upgrade/upgrade_test.go b/pkg/upgrade/upgrade_test.go index 3c0df6fb5..86e86ebe7 100644 --- a/pkg/upgrade/upgrade_test.go +++ b/pkg/upgrade/upgrade_test.go @@ -1,6 +1,7 @@ package upgrade import ( + "context" "os" "strings" "testing" @@ -73,7 +74,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 { @@ -89,6 +91,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.") } diff --git a/pkg/util/encoding/helper.go b/pkg/util/encoding/helper.go index d3aa23211..114cb6450 100644 --- a/pkg/util/encoding/helper.go +++ b/pkg/util/encoding/helper.go @@ -31,7 +31,7 @@ func Convert(from runtime.Object, to runtime.Object) error { } // ConvertList converts the objects from the from list and puts them into the to list -func ConvertList(fromList runtime.Object, toList runtime.Object, new rest.Storage) error { +func ConvertList(fromList runtime.Object, toList runtime.Object, newObj rest.Storage) error { list, err := meta.ExtractList(fromList) if err != nil { return err @@ -39,7 +39,7 @@ func ConvertList(fromList runtime.Object, toList runtime.Object, new rest.Storag newItems := []runtime.Object{} for _, item := range list { - newItem := new.New() + newItem := newObj.New() err = Convert(item, newItem) if err != nil { return err