|
| 1 | +package bundle |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/hex" |
| 6 | + "fmt" |
| 7 | + "io" |
| 8 | + "os" |
| 9 | + "path/filepath" |
| 10 | + "runtime" |
| 11 | + "strings" |
| 12 | + |
| 13 | + crcConfig "github.com/crc-org/crc/v2/pkg/crc/config" |
| 14 | + "github.com/crc-org/crc/v2/pkg/crc/constants" |
| 15 | + "github.com/crc-org/crc/v2/pkg/crc/gpg" |
| 16 | + "github.com/crc-org/crc/v2/pkg/crc/logging" |
| 17 | + "github.com/crc-org/crc/v2/pkg/crc/machine/bundle" |
| 18 | + crcPreset "github.com/crc-org/crc/v2/pkg/crc/preset" |
| 19 | + "github.com/crc-org/crc/v2/pkg/download" |
| 20 | + "github.com/spf13/cobra" |
| 21 | +) |
| 22 | + |
| 23 | +func getDownloadCmd(config *crcConfig.Config) *cobra.Command { |
| 24 | + downloadCmd := &cobra.Command{ |
| 25 | + Use: "download [version] [architecture]", |
| 26 | + Short: "Download a specific CRC bundle", |
| 27 | + 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.", |
| 28 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 29 | + force, _ := cmd.Flags().GetBool("force") |
| 30 | + presetStr, _ := cmd.Flags().GetString("preset") |
| 31 | + |
| 32 | + var preset crcPreset.Preset |
| 33 | + if presetStr != "" { |
| 34 | + var err error |
| 35 | + preset, err = crcPreset.ParsePresetE(presetStr) |
| 36 | + if err != nil { |
| 37 | + return err |
| 38 | + } |
| 39 | + } else { |
| 40 | + preset = crcConfig.GetPreset(config) |
| 41 | + } |
| 42 | + |
| 43 | + return runDownload(args, preset, force) |
| 44 | + }, |
| 45 | + } |
| 46 | + downloadCmd.Flags().BoolP("force", "f", false, "Overwrite existing bundle if present") |
| 47 | + downloadCmd.Flags().StringP("preset", "p", "", "Target preset (openshift, okd, microshift)") |
| 48 | + |
| 49 | + return downloadCmd |
| 50 | +} |
| 51 | + |
| 52 | +func runDownload(args []string, preset crcPreset.Preset, force bool) error { |
| 53 | + if len(args) > 2 { |
| 54 | + return fmt.Errorf("too many arguments: expected at most 2 (version, architecture), got %d", len(args)) |
| 55 | + } |
| 56 | + |
| 57 | + // If no args, use default bundle path |
| 58 | + if len(args) == 0 { |
| 59 | + defaultBundlePath := constants.GetDefaultBundlePath(preset) |
| 60 | + if !force { |
| 61 | + if _, err := os.Stat(defaultBundlePath); err == nil { |
| 62 | + logging.Infof("Bundle %s already exists. Use --force to overwrite.", defaultBundlePath) |
| 63 | + return nil |
| 64 | + } |
| 65 | + } |
| 66 | + |
| 67 | + logging.Debugf("Source: %s", constants.GetDefaultBundleDownloadURL(preset)) |
| 68 | + logging.Debugf("Destination: %s", defaultBundlePath) |
| 69 | + // For default bundle, we use the existing logic which handles verification internally |
| 70 | + _, err := bundle.Download(context.Background(), preset, defaultBundlePath, false) |
| 71 | + return err |
| 72 | + } |
| 73 | + |
| 74 | + // If args provided, we are constructing a URL |
| 75 | + version := args[0] |
| 76 | + |
| 77 | + // Check if version is partial (Major.Minor) and resolve it if necessary |
| 78 | + resolvedVersion, err := resolveOpenShiftVersion(preset, version) |
| 79 | + if err != nil { |
| 80 | + return fmt.Errorf("failed to resolve version %s: %w", version, err) |
| 81 | + } else if resolvedVersion != version { |
| 82 | + logging.Debugf("Resolved version %s to %s", version, resolvedVersion) |
| 83 | + version = resolvedVersion |
| 84 | + } |
| 85 | + |
| 86 | + architecture := runtime.GOARCH |
| 87 | + if len(args) > 1 { |
| 88 | + architecture = args[1] |
| 89 | + } |
| 90 | + |
| 91 | + bundleName := constants.BundleName(preset, version, architecture) |
| 92 | + bundlePath := filepath.Join(constants.MachineCacheDir, bundleName) |
| 93 | + |
| 94 | + if !force { |
| 95 | + if _, err := os.Stat(bundlePath); err == nil { |
| 96 | + logging.Infof("Bundle %s already exists. Use --force to overwrite.", bundleName) |
| 97 | + return nil |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + // Base URL for the directory containing the bundle and signature |
| 102 | + baseVersionURL := fmt.Sprintf("%s/%s/%s/", constants.DefaultMirrorURL, preset.String(), version) |
| 103 | + bundleURL := fmt.Sprintf("%s%s", baseVersionURL, bundleName) |
| 104 | + sigURL := fmt.Sprintf("%s%s", baseVersionURL, "sha256sum.txt.sig") |
| 105 | + |
| 106 | + logging.Infof("Downloading bundle: %s", bundleName) |
| 107 | + logging.Debugf("Source: %s", bundleURL) |
| 108 | + logging.Debugf("Destination: %s", constants.MachineCacheDir) |
| 109 | + |
| 110 | + // Implement verification logic |
| 111 | + logging.Infof("Verifying signature for %s...", version) |
| 112 | + sha256sum, err := getVerifiedHashForCustomVersion(sigURL, bundleName) |
| 113 | + if err != nil { |
| 114 | + // Fallback: try without .sig if .sig not found, maybe just sha256sum.txt? |
| 115 | + // For now, fail if signature verification fails as requested for "Safeguards" |
| 116 | + return fmt.Errorf("signature verification failed: %w", err) |
| 117 | + } |
| 118 | + |
| 119 | + sha256bytes, err := hex.DecodeString(sha256sum) |
| 120 | + if err != nil { |
| 121 | + return fmt.Errorf("failed to decode sha256sum: %w", err) |
| 122 | + } |
| 123 | + |
| 124 | + _, err = download.Download(context.Background(), bundleURL, bundlePath, 0664, sha256bytes) |
| 125 | + return err |
| 126 | +} |
| 127 | + |
| 128 | +func getVerifiedHashForCustomVersion(sigURL string, bundleName string) (string, error) { |
| 129 | + // Reuse existing verification logic from bundle package via a helper here |
| 130 | + // We essentially replicate getVerifiedHash but with our custom URL |
| 131 | + |
| 132 | + res, err := download.InMemory(sigURL) |
| 133 | + if err != nil { |
| 134 | + return "", fmt.Errorf("failed to fetch signature file: %w", err) |
| 135 | + } |
| 136 | + defer res.Close() |
| 137 | + |
| 138 | + signedHashes, err := io.ReadAll(res) |
| 139 | + if err != nil { |
| 140 | + return "", fmt.Errorf("failed to read signature file: %w", err) |
| 141 | + } |
| 142 | + |
| 143 | + verifiedHashes, err := gpg.GetVerifiedClearsignedMsgV3(constants.RedHatReleaseKey, string(signedHashes)) |
| 144 | + if err != nil { |
| 145 | + return "", fmt.Errorf("invalid signature: %w", err) |
| 146 | + } |
| 147 | + |
| 148 | + lines := strings.Split(verifiedHashes, "\n") |
| 149 | + for _, line := range lines { |
| 150 | + line = strings.TrimSpace(line) |
| 151 | + if strings.HasSuffix(line, bundleName) { |
| 152 | + sha256sum := strings.TrimSuffix(line, " "+bundleName) |
| 153 | + sha256sum = strings.TrimSpace(sha256sum) |
| 154 | + return sha256sum, nil |
| 155 | + } |
| 156 | + } |
| 157 | + return "", fmt.Errorf("hash for %s not found in signature file", bundleName) |
| 158 | +} |
0 commit comments