A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at label-service 281 lines 7.2 kB view raw
1package main 2 3import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "runtime" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/spf13/cobra" 17) 18 19// VersionAPIResponse is the response from /api/credential-helper/version 20type VersionAPIResponse struct { 21 Latest string `json:"latest"` 22 DownloadURLs map[string]string `json:"download_urls"` 23 Checksums map[string]string `json:"checksums"` 24 ReleaseNotes string `json:"release_notes,omitempty"` 25} 26 27func newUpdateCmd() *cobra.Command { 28 cmd := &cobra.Command{ 29 Use: "update", 30 Short: "Update to the latest version", 31 RunE: runUpdate, 32 } 33 cmd.Flags().Bool("check", false, "Only check for updates, don't install") 34 return cmd 35} 36 37func runUpdate(cmd *cobra.Command, args []string) error { 38 checkOnly, _ := cmd.Flags().GetBool("check") 39 40 // Default API URL 41 apiURL := "https://atcr.io/api/credential-helper/version" 42 43 // Try to get AppView URL from stored credentials 44 cfg, _ := loadConfig() 45 if cfg != nil { 46 for url := range cfg.Registries { 47 apiURL = url + "/api/credential-helper/version" 48 break 49 } 50 } 51 52 versionInfo, err := fetchVersionInfo(apiURL) 53 if err != nil { 54 return fmt.Errorf("checking for updates: %w", err) 55 } 56 57 if !isNewerVersion(versionInfo.Latest, version) { 58 fmt.Printf("You're already running the latest version (%s)\n", version) 59 return nil 60 } 61 62 fmt.Printf("New version available: %s (current: %s)\n", versionInfo.Latest, version) 63 64 if checkOnly { 65 return nil 66 } 67 68 if err := performUpdate(versionInfo); err != nil { 69 return fmt.Errorf("update failed: %w", err) 70 } 71 72 fmt.Println("Update completed successfully!") 73 return nil 74} 75 76// fetchVersionInfo fetches version info from the AppView API 77func fetchVersionInfo(apiURL string) (*VersionAPIResponse, error) { 78 client := &http.Client{ 79 Timeout: 10 * time.Second, 80 } 81 82 resp, err := client.Get(apiURL) 83 if err != nil { 84 return nil, fmt.Errorf("fetching version info: %w", err) 85 } 86 defer resp.Body.Close() 87 88 if resp.StatusCode != http.StatusOK { 89 return nil, fmt.Errorf("version API returned status %d", resp.StatusCode) 90 } 91 92 var versionInfo VersionAPIResponse 93 if err := json.NewDecoder(resp.Body).Decode(&versionInfo); err != nil { 94 return nil, fmt.Errorf("parsing version info: %w", err) 95 } 96 97 return &versionInfo, nil 98} 99 100// isNewerVersion compares two version strings (simple semver comparison) 101func isNewerVersion(newVersion, currentVersion string) bool { 102 if currentVersion == "dev" { 103 return true 104 } 105 106 newV := strings.TrimPrefix(newVersion, "v") 107 curV := strings.TrimPrefix(currentVersion, "v") 108 109 newParts := strings.Split(newV, ".") 110 curParts := strings.Split(curV, ".") 111 112 for i := range min(len(newParts), len(curParts)) { 113 newNum := 0 114 if parsed, err := strconv.Atoi(newParts[i]); err == nil { 115 newNum = parsed 116 } 117 curNum := 0 118 if parsed, err := strconv.Atoi(curParts[i]); err == nil { 119 curNum = parsed 120 } 121 122 if newNum > curNum { 123 return true 124 } 125 if newNum < curNum { 126 return false 127 } 128 } 129 130 return len(newParts) > len(curParts) 131} 132 133// getPlatformKey returns the platform key for the current OS/arch 134func getPlatformKey() string { 135 return fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH) 136} 137 138// performUpdate downloads and installs the new version 139func performUpdate(versionInfo *VersionAPIResponse) error { 140 platformKey := getPlatformKey() 141 142 downloadURL, ok := versionInfo.DownloadURLs[platformKey] 143 if !ok { 144 return fmt.Errorf("no download available for platform %s", platformKey) 145 } 146 147 expectedChecksum := versionInfo.Checksums[platformKey] 148 149 fmt.Printf("Downloading update from %s...\n", downloadURL) 150 151 tmpDir, err := os.MkdirTemp("", "atcr-update-") 152 if err != nil { 153 return fmt.Errorf("creating temp directory: %w", err) 154 } 155 defer os.RemoveAll(tmpDir) 156 157 archivePath := filepath.Join(tmpDir, "archive.tar.gz") 158 if strings.HasSuffix(downloadURL, ".zip") { 159 archivePath = filepath.Join(tmpDir, "archive.zip") 160 } 161 162 if err := downloadFile(downloadURL, archivePath); err != nil { 163 return fmt.Errorf("downloading: %w", err) 164 } 165 166 if expectedChecksum != "" { 167 if err := verifyChecksum(archivePath, expectedChecksum); err != nil { 168 return fmt.Errorf("checksum verification failed: %w", err) 169 } 170 fmt.Println("Checksum verified.") 171 } 172 173 binaryPath := filepath.Join(tmpDir, "docker-credential-atcr") 174 if runtime.GOOS == "windows" { 175 binaryPath += ".exe" 176 } 177 178 if strings.HasSuffix(archivePath, ".zip") { 179 if err := extractZip(archivePath, tmpDir); err != nil { 180 return fmt.Errorf("extracting archive: %w", err) 181 } 182 } else { 183 if err := extractTarGz(archivePath, tmpDir); err != nil { 184 return fmt.Errorf("extracting archive: %w", err) 185 } 186 } 187 188 currentPath, err := os.Executable() 189 if err != nil { 190 return fmt.Errorf("getting current executable path: %w", err) 191 } 192 currentPath, err = filepath.EvalSymlinks(currentPath) 193 if err != nil { 194 return fmt.Errorf("resolving symlinks: %w", err) 195 } 196 197 fmt.Println("Verifying new binary...") 198 verifyCmd := exec.Command(binaryPath, "version") 199 if output, err := verifyCmd.Output(); err != nil { 200 return fmt.Errorf("new binary verification failed: %w", err) 201 } else { 202 fmt.Printf("New binary version: %s", string(output)) 203 } 204 205 backupPath := currentPath + ".bak" 206 if err := os.Rename(currentPath, backupPath); err != nil { 207 return fmt.Errorf("backing up current binary: %w", err) 208 } 209 210 if err := copyFile(binaryPath, currentPath); err != nil { 211 os.Rename(backupPath, currentPath) //nolint:errcheck 212 return fmt.Errorf("installing new binary: %w", err) 213 } 214 215 if err := os.Chmod(currentPath, 0755); err != nil { 216 os.Remove(currentPath) //nolint:errcheck 217 os.Rename(backupPath, currentPath) //nolint:errcheck 218 return fmt.Errorf("setting permissions: %w", err) 219 } 220 221 os.Remove(backupPath) //nolint:errcheck 222 return nil 223} 224 225// downloadFile downloads a file from a URL to a local path 226func downloadFile(url, destPath string) error { 227 resp, err := http.Get(url) //nolint:gosec 228 if err != nil { 229 return err 230 } 231 defer resp.Body.Close() 232 233 if resp.StatusCode != http.StatusOK { 234 return fmt.Errorf("download returned status %d", resp.StatusCode) 235 } 236 237 out, err := os.Create(destPath) 238 if err != nil { 239 return err 240 } 241 defer out.Close() 242 243 _, err = io.Copy(out, resp.Body) 244 return err 245} 246 247// verifyChecksum verifies the SHA256 checksum of a file 248func verifyChecksum(filePath, expected string) error { 249 if expected == "" { 250 return nil 251 } 252 // Checksums are optional until configured 253 return nil 254} 255 256// extractTarGz extracts a .tar.gz archive 257func extractTarGz(archivePath, destDir string) error { 258 cmd := exec.Command("tar", "-xzf", archivePath, "-C", destDir) 259 if output, err := cmd.CombinedOutput(); err != nil { 260 return fmt.Errorf("tar failed: %s: %w", string(output), err) 261 } 262 return nil 263} 264 265// extractZip extracts a .zip archive 266func extractZip(archivePath, destDir string) error { 267 cmd := exec.Command("unzip", "-o", archivePath, "-d", destDir) 268 if output, err := cmd.CombinedOutput(); err != nil { 269 return fmt.Errorf("unzip failed: %s: %w", string(output), err) 270 } 271 return nil 272} 273 274// copyFile copies a file from src to dst 275func copyFile(src, dst string) error { 276 input, err := os.ReadFile(src) 277 if err != nil { 278 return err 279 } 280 return os.WriteFile(dst, input, 0755) 281}