-
Notifications
You must be signed in to change notification settings - Fork 256
PoC: Add bundle download cmd #5037
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sebrandon1
wants to merge
1
commit into
crc-org:main
Choose a base branch
from
sebrandon1:add_bundle_download_cmd
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package bundle | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
||
| "github.com/crc-org/crc/v2/pkg/crc/constants" | ||
| "github.com/crc-org/crc/v2/pkg/crc/logging" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| func getClearCmd() *cobra.Command { | ||
| return &cobra.Command{ | ||
| Use: "clear", | ||
| Short: "Clear cached CRC bundles", | ||
| Long: "Delete all downloaded CRC bundles from the cache directory.", | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| return runClear() | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func runClear() error { | ||
| cacheDir := constants.MachineCacheDir | ||
| if _, err := os.Stat(cacheDir); os.IsNotExist(err) { | ||
| logging.Infof("Cache directory %s does not exist", cacheDir) | ||
| return nil | ||
| } | ||
|
|
||
| files, err := os.ReadDir(cacheDir) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| cleared := false | ||
| for _, file := range files { | ||
| if strings.HasSuffix(file.Name(), ".crcbundle") { | ||
| filePath := filepath.Join(cacheDir, file.Name()) | ||
| logging.Infof("Deleting %s", filePath) | ||
| if err := os.RemoveAll(filePath); err != nil { | ||
| logging.Errorf("Failed to remove %s: %v", filePath, err) | ||
| } | ||
| cleared = true | ||
| } | ||
| } | ||
|
|
||
| if !cleared { | ||
| logging.Infof("No bundles found in %s", cacheDir) | ||
| } else { | ||
| logging.Infof("Cleared cached bundles in %s", cacheDir) | ||
| } | ||
| return nil | ||
| } | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| package bundle | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/hex" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "path/filepath" | ||
| "runtime" | ||
| "strings" | ||
|
|
||
| crcConfig "github.com/crc-org/crc/v2/pkg/crc/config" | ||
| "github.com/crc-org/crc/v2/pkg/crc/constants" | ||
| "github.com/crc-org/crc/v2/pkg/crc/gpg" | ||
| "github.com/crc-org/crc/v2/pkg/crc/logging" | ||
| "github.com/crc-org/crc/v2/pkg/crc/machine/bundle" | ||
| crcPreset "github.com/crc-org/crc/v2/pkg/crc/preset" | ||
| "github.com/crc-org/crc/v2/pkg/download" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| func getDownloadCmd(config *crcConfig.Config) *cobra.Command { | ||
| downloadCmd := &cobra.Command{ | ||
| Use: "download [version] [architecture]", | ||
| Short: "Download a specific CRC bundle", | ||
| Long: "Download a specific CRC bundle from the mirrors. If no version or architecture is specified, the bundle for the current CRC version will be downloaded.", | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| force, _ := cmd.Flags().GetBool("force") | ||
| presetStr, _ := cmd.Flags().GetString("preset") | ||
|
|
||
| var preset crcPreset.Preset | ||
| if presetStr != "" { | ||
| var err error | ||
| preset, err = crcPreset.ParsePresetE(presetStr) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } else { | ||
| preset = crcConfig.GetPreset(config) | ||
| } | ||
|
|
||
| return runDownload(args, preset, force) | ||
| }, | ||
| } | ||
| downloadCmd.Flags().BoolP("force", "f", false, "Overwrite existing bundle if present") | ||
| downloadCmd.Flags().StringP("preset", "p", "", "Target preset (openshift, okd, microshift)") | ||
|
|
||
| return downloadCmd | ||
| } | ||
|
|
||
| func runDownload(args []string, preset crcPreset.Preset, force bool) error { | ||
| // Disk space check (simple check for ~10GB free) | ||
| // This is a basic check, more robust checking would require syscall/windows specific implementations | ||
| // We skip this for now to avoid adding heavy OS-specific deps, assuming user manages disk space or download fails naturally. | ||
|
|
||
| // If no args, use default bundle path | ||
| if len(args) == 0 { | ||
| defaultBundlePath := constants.GetDefaultBundlePath(preset) | ||
| if !force { | ||
| if _, err := os.Stat(defaultBundlePath); err == nil { | ||
| logging.Infof("Bundle %s already exists. Use --force to overwrite.", defaultBundlePath) | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| logging.Debugf("Source: %s", constants.GetDefaultBundleDownloadURL(preset)) | ||
| logging.Debugf("Destination: %s", defaultBundlePath) | ||
| // For default bundle, we use the existing logic which handles verification internally | ||
| _, err := bundle.Download(context.Background(), preset, defaultBundlePath, false) | ||
| return err | ||
| } | ||
|
|
||
| // If args provided, we are constructing a URL | ||
| version := args[0] | ||
|
|
||
| // Check if version is partial (Major.Minor) and resolve it if necessary | ||
| resolvedVersion, err := resolveOpenShiftVersion(preset, version) | ||
| if err != nil { | ||
| logging.Warnf("Could not resolve version %s: %v. Trying with original version string.", version, err) | ||
| } else if resolvedVersion != version { | ||
| logging.Debugf("Resolved version %s to %s", version, resolvedVersion) | ||
| version = resolvedVersion | ||
| } | ||
|
|
||
| architecture := runtime.GOARCH | ||
| if len(args) > 1 { | ||
| architecture = args[1] | ||
| } | ||
|
|
||
| bundleName := constants.BundleName(preset, version, architecture) | ||
| bundlePath := filepath.Join(constants.MachineCacheDir, bundleName) | ||
|
|
||
| if !force { | ||
| if _, err := os.Stat(bundlePath); err == nil { | ||
| logging.Infof("Bundle %s already exists. Use --force to overwrite.", bundleName) | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| // Base URL for the directory containing the bundle and signature | ||
| baseVersionURL := fmt.Sprintf("%s/%s/%s/", constants.DefaultMirrorURL, preset.String(), version) | ||
| bundleURL := fmt.Sprintf("%s%s", baseVersionURL, bundleName) | ||
| sigURL := fmt.Sprintf("%s%s", baseVersionURL, "sha256sum.txt.sig") | ||
|
|
||
| logging.Infof("Downloading bundle: %s", bundleName) | ||
| logging.Debugf("Source: %s", bundleURL) | ||
| logging.Debugf("Destination: %s", constants.MachineCacheDir) | ||
|
|
||
| // Implement verification logic | ||
| logging.Infof("Verifying signature for %s...", version) | ||
| sha256sum, err := getVerifiedHashForCustomVersion(sigURL, bundleName) | ||
| if err != nil { | ||
| // Fallback: try without .sig if .sig not found, maybe just sha256sum.txt? | ||
| // For now, fail if signature verification fails as requested for "Safeguards" | ||
| return fmt.Errorf("signature verification failed: %w", err) | ||
| } | ||
|
|
||
| sha256bytes, err := hex.DecodeString(sha256sum) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to decode sha256sum: %w", err) | ||
| } | ||
|
|
||
| _, err = download.Download(context.Background(), bundleURL, bundlePath, 0664, sha256bytes) | ||
| return err | ||
| } | ||
|
|
||
| func getVerifiedHashForCustomVersion(sigURL string, bundleName string) (string, error) { | ||
| // Reuse existing verification logic from bundle package via a helper here | ||
| // We essentially replicate getVerifiedHash but with our custom URL | ||
|
|
||
| res, err := download.InMemory(sigURL) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to fetch signature file: %w", err) | ||
| } | ||
| defer res.Close() | ||
|
|
||
| signedHashes, err := io.ReadAll(res) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to read signature file: %w", err) | ||
| } | ||
|
|
||
| verifiedHashes, err := gpg.GetVerifiedClearsignedMsgV3(constants.RedHatReleaseKey, string(signedHashes)) | ||
| if err != nil { | ||
| return "", fmt.Errorf("invalid signature: %w", err) | ||
| } | ||
|
|
||
| lines := strings.Split(verifiedHashes, "\n") | ||
| for _, line := range lines { | ||
| if strings.HasSuffix(line, bundleName) { | ||
| sha256sum := strings.TrimSuffix(line, " "+bundleName) | ||
| return sha256sum, nil | ||
| } | ||
| } | ||
| return "", fmt.Errorf("hash for %s not found in signature file", bundleName) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package bundle | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "github.com/Masterminds/semver/v3" | ||
| crcConfig "github.com/crc-org/crc/v2/pkg/crc/config" | ||
| "github.com/crc-org/crc/v2/pkg/crc/logging" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| func getListCmd(config *crcConfig.Config) *cobra.Command { | ||
| return &cobra.Command{ | ||
| Use: "list [version]", | ||
| Short: "List available CRC bundles", | ||
| Long: "List available CRC bundles from the mirrors. Optionally filter by major.minor version (e.g. 4.19).", | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| return runList(args, config) | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func runList(args []string, config *crcConfig.Config) error { | ||
| preset := crcConfig.GetPreset(config) | ||
| versions, err := fetchAvailableVersions(preset) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if len(versions) == 0 { | ||
| logging.Infof("No bundles found for preset %s", preset) | ||
| return nil | ||
| } | ||
|
|
||
| var filter *semver.Version | ||
| if len(args) > 0 { | ||
| v, err := semver.NewVersion(args[0] + ".0") // Treat 4.19 as 4.19.0 for partial matching | ||
| if err == nil { | ||
| filter = v | ||
| } else { | ||
| // Try parsing as full version just in case | ||
| v, err = semver.NewVersion(args[0]) | ||
| if err == nil { | ||
| filter = v | ||
| } | ||
| } | ||
| } | ||
|
|
||
| logging.Infof("Available bundles for %s:", preset) | ||
| for _, v := range versions { | ||
| if filter != nil { | ||
| if v.Major() != filter.Major() || v.Minor() != filter.Minor() { | ||
| continue | ||
| } | ||
| } | ||
|
|
||
| cachedStr := "" | ||
| if isBundleCached(preset, v.String()) { | ||
| cachedStr = " (cached)" | ||
| } | ||
| fmt.Printf("%s%s\n", v.String(), cachedStr) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| package bundle | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "sort" | ||
| "strings" | ||
|
|
||
| "github.com/crc-org/crc/v2/pkg/crc/constants" | ||
| "github.com/crc-org/crc/v2/pkg/crc/logging" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| func getPruneCmd() *cobra.Command { | ||
| return &cobra.Command{ | ||
| Use: "prune", | ||
| Short: "Prune old CRC bundles", | ||
| Long: "Keep only the most recent bundles and delete older ones to save space.", | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| // Default keep 2 most recent | ||
| return runPrune(2) | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func runPrune(keep int) error { | ||
| cacheDir := constants.MachineCacheDir | ||
| if _, err := os.Stat(cacheDir); os.IsNotExist(err) { | ||
| logging.Infof("Cache directory %s does not exist", cacheDir) | ||
| return nil | ||
| } | ||
|
|
||
| files, err := os.ReadDir(cacheDir) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| var bundleFiles []os.DirEntry | ||
| for _, file := range files { | ||
| if strings.HasSuffix(file.Name(), ".crcbundle") { | ||
| bundleFiles = append(bundleFiles, file) | ||
| } | ||
| } | ||
|
|
||
| if len(bundleFiles) <= keep { | ||
| logging.Infof("Nothing to prune (found %d bundles, keeping %d)", len(bundleFiles), keep) | ||
| return nil | ||
| } | ||
|
|
||
| // Sort by modification time, newest first | ||
| sort.Slice(bundleFiles, func(i, j int) bool { | ||
| infoI, _ := bundleFiles[i].Info() | ||
| infoJ, _ := bundleFiles[j].Info() | ||
| return infoI.ModTime().After(infoJ.ModTime()) | ||
| }) | ||
|
|
||
| for i := keep; i < len(bundleFiles); i++ { | ||
| file := bundleFiles[i] | ||
| filePath := filepath.Join(cacheDir, file.Name()) | ||
| logging.Infof("Pruning old bundle: %s", file.Name()) | ||
| if err := os.RemoveAll(filePath); err != nil { | ||
| logging.Errorf("Failed to remove %s: %v", filePath, err) | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Add validation for the number of arguments.
The function checks
len(args) > 0to determine if a filter is provided, but doesn't validate that at most one argument is supplied. If a user provides multiple arguments (e.g.,crc bundle list 4.19 4.20), only the first is used and the rest are silently ignored, which may confuse users.🔎 Apply this diff to add argument validation:
func runList(args []string, config *crcConfig.Config) error { + if len(args) > 1 { + return fmt.Errorf("too many arguments: expected at most 1 version filter, got %d", len(args)) + } + preset := crcConfig.GetPreset(config) versions, err := fetchAvailableVersions(preset)📝 Committable suggestion
🤖 Prompt for AI Agents