From 4fdc52e7602f8daed5fdfa7794f20182195b58a4 Mon Sep 17 00:00:00 2001 From: QuintenQVD0 <josdekurk@gmail.com> Date: Mon, 2 Dec 2024 17:53:10 +0100 Subject: [PATCH 1/7] add selfupdate --- cmd/root.go | 23 +++-- cmd/selfupdate.go | 257 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+), 11 deletions(-) create mode 100644 cmd/selfupdate.go diff --git a/cmd/root.go b/cmd/root.go index 258858f..2a67dd6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -88,23 +88,24 @@ func init() { rootCommand.AddCommand(versionCommand) rootCommand.AddCommand(configureCmd) rootCommand.AddCommand(newDiagnosticsCommand()) + rootCommand.AddCommand(newSelfupdateCommand()) } func isDockerSnap() bool { - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - log.Fatalf("Unable to initialize Docker client: %s", err) - } + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + log.Fatalf("Unable to initialize Docker client: %s", err) + } - defer cli.Close() // Close the client when the function returns (should not be needed, but just to be safe) + defer cli.Close() // Close the client when the function returns (should not be needed, but just to be safe) - info, err := cli.Info(context.Background()) - if err != nil { - log.Fatalf("Unable to get Docker info: %s", err) - } + info, err := cli.Info(context.Background()) + if err != nil { + log.Fatalf("Unable to get Docker info: %s", err) + } - // Check if Docker root directory contains '/var/snap/docker' - return strings.Contains(info.DockerRootDir, "/var/snap/docker") + // Check if Docker root directory contains '/var/snap/docker' + return strings.Contains(info.DockerRootDir, "/var/snap/docker") } func rootCmdRun(cmd *cobra.Command, _ []string) { diff --git a/cmd/selfupdate.go b/cmd/selfupdate.go new file mode 100644 index 0000000..498d753 --- /dev/null +++ b/cmd/selfupdate.go @@ -0,0 +1,257 @@ +package cmd + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "runtime" + "strings" + "time" + + "github.com/pelican-dev/wings/system" + "github.com/spf13/cobra" +) + +var updateArgs struct { + repoOwner string + repoName string +} + +func newSelfupdateCommand() *cobra.Command { + command := &cobra.Command{ + Use: "update", + Short: "Update the wings to the latest version", + Run: selfupdateCmdRun, + } + + command.Flags().StringVar(&updateArgs.repoOwner, "repo-owner", "pelican-dev", "GitHub username or organization that owns the repository containing the updates") + command.Flags().StringVar(&updateArgs.repoName, "repo-name", "wings", "The name of the GitHub repository to fetch updates from") + + return command +} + +func selfupdateCmdRun(*cobra.Command, []string) { + currentVersion := system.Version + if currentVersion == "" { + fmt.Println("Error: Current version is not defined") + return + } + + if currentVersion == "develop" { + fmt.Println("Running in development mode. Skipping update.") + return + } + + fmt.Println("Current version:", currentVersion) + + // Fetch the latest release tag from GitHub API + latestVersionTag, err := fetchLatestGitHubRelease() + if err != nil { + fmt.Println("Failed to fetch the latest version:", err) + return + } + + currentVersionTag := "v" + currentVersion + if latestVersionTag == currentVersionTag { + fmt.Println("You are running the latest version:", currentVersion) + return + } + + fmt.Printf("A new version is available: %s (current: %s)\n", latestVersionTag, currentVersionTag) + + binaryName := determineBinaryName() + if binaryName == "" { + fmt.Println("Unsupported architecture") + return + } + + downloadURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", updateArgs.repoOwner, updateArgs.repoName, latestVersionTag, binaryName) + checksumURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/checksums.txt", updateArgs.repoOwner, updateArgs.repoName, latestVersionTag) + + fmt.Println("Downloading checksums.txt...") + checksumFile, err := downloadFile(checksumURL, "checksums.txt") + if err != nil { + fmt.Println("Failed to download checksum file:", err) + return + } + defer os.Remove(checksumFile) + + fmt.Println("Downloading", binaryName, "...") + binaryFile, err := downloadFile(downloadURL, binaryName) + if err != nil { + fmt.Println("Failed to download binary file:", err) + return + } + defer os.Remove(binaryFile) + + if err := verifyChecksum(binaryFile, checksumFile, binaryName); err != nil { + fmt.Println("Checksum verification failed:", err) + return + } + fmt.Println("\nChecksum verification successful.") + + currentExecutable, err := os.Executable() + if err != nil { + fmt.Println("Failed to locate current executable:", err) + return + } + + if err := os.Chmod(binaryFile, 0755); err != nil { + fmt.Println("Failed to set executable permissions on the new binary:", err) + return + } + + if err := replaceBinary(currentExecutable, binaryFile); err != nil { + fmt.Println("Failed to replace executable:", err) + return + } + + fmt.Println("Restarting service...") + + if err := restartService(); err != nil { + fmt.Println("Error restarting the wings service:", err) + } else { + fmt.Println("Service restarted successfully.") + } +} + +func fetchLatestGitHubRelease() (string, error) { + apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", updateArgs.repoOwner, updateArgs.repoName) + + resp, err := http.Get(apiURL) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var releaseData struct { + TagName string `json:"tag_name"` + } + if err := json.NewDecoder(resp.Body).Decode(&releaseData); err != nil { + return "", err + } + + return releaseData.TagName, nil +} + +func determineBinaryName() string { + switch runtime.GOARCH { + case "amd64": + return "wings_linux_amd64" + case "arm64": + return "wings_linux_arm64" + default: + return "" + } +} + +func downloadFile(url, fileName string) (string, error) { + tmpFile, err := os.CreateTemp("", fileName) + if err != nil { + return "", err + } + defer tmpFile.Close() + + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status: %s", resp.Status) + } + + fmt.Printf("Downloading %s (%.2f MB)...\n", fileName, float64(resp.ContentLength)/1024/1024) + progressWriter := &progressWriter{Writer: tmpFile, Total: resp.ContentLength} + if _, err := io.Copy(progressWriter, resp.Body); err != nil { + return "", err + } + + fmt.Println() // Ensure a newline after download progress + return tmpFile.Name(), nil +} + +func verifyChecksum(binaryPath, checksumPath, binaryName string) error { + checksumData, err := os.ReadFile(checksumPath) + if err != nil { + return err + } + + var expectedChecksum string + for _, line := range strings.Split(string(checksumData), "\n") { + if strings.HasSuffix(line, binaryName) { + parts := strings.Fields(line) + if len(parts) > 0 { + expectedChecksum = parts[0] + } + break + } + } + if expectedChecksum == "" { + return fmt.Errorf("checksum not found for %s", binaryName) + } + + file, err := os.Open(binaryPath) + if err != nil { + return err + } + defer file.Close() + + hasher := sha256.New() + if _, err := io.Copy(hasher, file); err != nil { + return err + } + actualChecksum := fmt.Sprintf("%x", hasher.Sum(nil)) + + if actualChecksum != expectedChecksum { + return fmt.Errorf("checksum mismatch: expected %s, got %s", expectedChecksum, actualChecksum) + } + + return nil +} + +func replaceBinary(currentPath, newPath string) error { + return os.Rename(newPath, currentPath) +} + +type progressWriter struct { + io.Writer + Total int64 + Written int64 + StartTime time.Time +} + +func (pw *progressWriter) Write(p []byte) (int, error) { + n, err := pw.Writer.Write(p) + pw.Written += int64(n) + + if pw.Total > 0 { + percent := float64(pw.Written) / float64(pw.Total) * 100 + fmt.Printf("\rProgress: %.2f%%", percent) + } + + return n, err +} + +func restartService() error { + // Try to run the systemctl restart command + cmd := exec.Command("systemctl", "restart", "wings") + cmdOutput, err := cmd.CombinedOutput() + + if err != nil { + // If systemctl command fails, return the error with output + return fmt.Errorf("failed to restart service: %s\n%s", err.Error(), string(cmdOutput)) + } + + // If successful, return nil + return nil +} From b14696d49d30695dc0836450dda8c2e99f6d2be5 Mon Sep 17 00:00:00 2001 From: QuintenQVD0 <josdekurk@gmail.com> Date: Mon, 2 Dec 2024 18:02:11 +0100 Subject: [PATCH 2/7] Bump golang 1.23 sub version --- .github/workflows/push.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 8abca1b..7bb52aa 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-22.04] - go: ["1.22.7", "1.23.1"] + go: ["1.22.7", "1.23.3"] goos: [linux] goarch: [amd64, arm64] From b7e1b53341d374945927e6f7234ea4b299ece3f2 Mon Sep 17 00:00:00 2001 From: Quinten <67589015+QuintenQVD0@users.noreply.github.com> Date: Sat, 7 Dec 2024 13:40:53 +0100 Subject: [PATCH 3/7] Remove systemd restart from autoupdate --- cmd/selfupdate.go | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/cmd/selfupdate.go b/cmd/selfupdate.go index 498d753..fef969e 100644 --- a/cmd/selfupdate.go +++ b/cmd/selfupdate.go @@ -110,13 +110,8 @@ func selfupdateCmdRun(*cobra.Command, []string) { return } - fmt.Println("Restarting service...") + fmt.Println("Now restart the wings service. Example: systemctl restart wings") - if err := restartService(); err != nil { - fmt.Println("Error restarting the wings service:", err) - } else { - fmt.Println("Service restarted successfully.") - } } func fetchLatestGitHubRelease() (string, error) { @@ -241,17 +236,3 @@ func (pw *progressWriter) Write(p []byte) (int, error) { return n, err } - -func restartService() error { - // Try to run the systemctl restart command - cmd := exec.Command("systemctl", "restart", "wings") - cmdOutput, err := cmd.CombinedOutput() - - if err != nil { - // If systemctl command fails, return the error with output - return fmt.Errorf("failed to restart service: %s\n%s", err.Error(), string(cmdOutput)) - } - - // If successful, return nil - return nil -} From 9f7745f2fc6e37c9bd68187299e347f2acb46def Mon Sep 17 00:00:00 2001 From: Quinten <67589015+QuintenQVD0@users.noreply.github.com> Date: Sat, 7 Dec 2024 13:47:43 +0100 Subject: [PATCH 4/7] we don't need os exec anymore --- cmd/selfupdate.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/selfupdate.go b/cmd/selfupdate.go index fef969e..75b4a91 100644 --- a/cmd/selfupdate.go +++ b/cmd/selfupdate.go @@ -7,7 +7,6 @@ import ( "io" "net/http" "os" - "os/exec" "runtime" "strings" "time" From 1d19608bc5fe0641948e67293a0be9e9b7a7e407 Mon Sep 17 00:00:00 2001 From: Quinten <67589015+QuintenQVD0@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:52:08 +0100 Subject: [PATCH 5/7] cleanup --- cmd/selfupdate.go | 156 +++++++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 71 deletions(-) diff --git a/cmd/selfupdate.go b/cmd/selfupdate.go index 75b4a91..e0b83f6 100644 --- a/cmd/selfupdate.go +++ b/cmd/selfupdate.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "os" + "path/filepath" "runtime" "strings" "time" @@ -18,99 +19,139 @@ import ( var updateArgs struct { repoOwner string repoName string + force bool } func newSelfupdateCommand() *cobra.Command { command := &cobra.Command{ Use: "update", - Short: "Update the wings to the latest version", + Short: "Update wings to the latest version", Run: selfupdateCmdRun, } - command.Flags().StringVar(&updateArgs.repoOwner, "repo-owner", "pelican-dev", "GitHub username or organization that owns the repository containing the updates") - command.Flags().StringVar(&updateArgs.repoName, "repo-name", "wings", "The name of the GitHub repository to fetch updates from") + command.Flags().StringVar(&updateArgs.repoOwner, "repo-owner", "pelican-dev", "GitHub repository owner") + command.Flags().StringVar(&updateArgs.repoName, "repo-name", "wings", "GitHub repository name") + command.Flags().BoolVar(&updateArgs.force, "force", false, "Force update even if on latest version") return command } -func selfupdateCmdRun(*cobra.Command, []string) { +func selfupdateCmdRun(_ *cobra.Command, _ []string) { currentVersion := system.Version if currentVersion == "" { - fmt.Println("Error: Current version is not defined") + fmt.Println("Error: current version is not defined") return } - if currentVersion == "develop" { - fmt.Println("Running in development mode. Skipping update.") + if currentVersion == "develop" && !updateArgs.force { + fmt.Println("Running in development mode. Use --force to override.") return } - fmt.Println("Current version:", currentVersion) + fmt.Printf("Current version: %s\n", currentVersion) // Fetch the latest release tag from GitHub API latestVersionTag, err := fetchLatestGitHubRelease() if err != nil { - fmt.Println("Failed to fetch the latest version:", err) + fmt.Printf("Failed to fetch latest version: %v\n", err) return } - currentVersionTag := "v" + currentVersion - if latestVersionTag == currentVersionTag { - fmt.Println("You are running the latest version:", currentVersion) - return + var currentVersionTag string + if currentVersion != "develop" { + currentVersionTag = "v" + currentVersion + } else { + currentVersionTag = "develop" } - fmt.Printf("A new version is available: %s (current: %s)\n", latestVersionTag, currentVersionTag) + if latestVersionTag == currentVersionTag && !updateArgs.force { + fmt.Printf("You are running the latest version: %s\n", currentVersion) + return + } binaryName := determineBinaryName() if binaryName == "" { - fmt.Println("Unsupported architecture") + fmt.Printf("Error: unsupported architecture: %s\n", runtime.GOARCH) return } - downloadURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", updateArgs.repoOwner, updateArgs.repoName, latestVersionTag, binaryName) - checksumURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/checksums.txt", updateArgs.repoOwner, updateArgs.repoName, latestVersionTag) + fmt.Printf("Updating from %s to %s\n", currentVersionTag, latestVersionTag) - fmt.Println("Downloading checksums.txt...") - checksumFile, err := downloadFile(checksumURL, "checksums.txt") - if err != nil { - fmt.Println("Failed to download checksum file:", err) + if err := performUpdate(latestVersionTag, binaryName); err != nil { + fmt.Printf("Update failed: %v\n", err) return } - defer os.Remove(checksumFile) - fmt.Println("Downloading", binaryName, "...") - binaryFile, err := downloadFile(downloadURL, binaryName) + fmt.Println("\nUpdate successful! Please restart the wings service (e.g., systemctl restart wings)") +} + +func performUpdate(version, binaryName string) error { + downloadURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", + updateArgs.repoOwner, updateArgs.repoName, version, binaryName) + checksumURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/checksums.txt", + updateArgs.repoOwner, updateArgs.repoName, version) + + tmpDir, err := os.MkdirTemp("", "wings-update-*") if err != nil { - fmt.Println("Failed to download binary file:", err) - return + return fmt.Errorf("failed to create temp directory: %v", err) } - defer os.Remove(binaryFile) + defer os.RemoveAll(tmpDir) - if err := verifyChecksum(binaryFile, checksumFile, binaryName); err != nil { - fmt.Println("Checksum verification failed:", err) - return + checksumPath := filepath.Join(tmpDir, "checksums.txt") + if err := downloadWithProgress(checksumURL, checksumPath); err != nil { + return fmt.Errorf("failed to download checksums: %v", err) + } + + binaryPath := filepath.Join(tmpDir, binaryName) + if err := downloadWithProgress(downloadURL, binaryPath); err != nil { + return fmt.Errorf("failed to download binary: %v", err) + } + + if err := verifyChecksum(binaryPath, checksumPath, binaryName); err != nil { + return fmt.Errorf("checksum verification failed: %v", err) + } + + if err := os.Chmod(binaryPath, 0755); err != nil { + return fmt.Errorf("failed to set executable permissions: %v", err) } - fmt.Println("\nChecksum verification successful.") currentExecutable, err := os.Executable() if err != nil { - fmt.Println("Failed to locate current executable:", err) - return + return fmt.Errorf("failed to locate current executable: %v", err) } - if err := os.Chmod(binaryFile, 0755); err != nil { - fmt.Println("Failed to set executable permissions on the new binary:", err) - return + return os.Rename(binaryPath, currentExecutable) +} + +func downloadWithProgress(url, dest string) error { + resp, err := http.Get(url) + if err != nil { + return err } + defer resp.Body.Close() - if err := replaceBinary(currentExecutable, binaryFile); err != nil { - fmt.Println("Failed to replace executable:", err) - return + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status: %s", resp.Status) } - fmt.Println("Now restart the wings service. Example: systemctl restart wings") + out, err := os.Create(dest) + if err != nil { + return err + } + defer out.Close() + + filename := filepath.Base(dest) + fmt.Printf("Downloading %s (%.2f MB)...\n", filename, float64(resp.ContentLength)/1024/1024) + + pw := &progressWriter{ + Writer: out, + Total: resp.ContentLength, + StartTime: time.Now(), + } + _, err = io.Copy(pw, resp.Body) + fmt.Println() + return err } func fetchLatestGitHubRelease() (string, error) { @@ -147,33 +188,6 @@ func determineBinaryName() string { } } -func downloadFile(url, fileName string) (string, error) { - tmpFile, err := os.CreateTemp("", fileName) - if err != nil { - return "", err - } - defer tmpFile.Close() - - resp, err := http.Get(url) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("unexpected status: %s", resp.Status) - } - - fmt.Printf("Downloading %s (%.2f MB)...\n", fileName, float64(resp.ContentLength)/1024/1024) - progressWriter := &progressWriter{Writer: tmpFile, Total: resp.ContentLength} - if _, err := io.Copy(progressWriter, resp.Body); err != nil { - return "", err - } - - fmt.Println() // Ensure a newline after download progress - return tmpFile.Name(), nil -} - func verifyChecksum(binaryPath, checksumPath, binaryName string) error { checksumData, err := os.ReadFile(checksumPath) if err != nil { @@ -206,6 +220,10 @@ func verifyChecksum(binaryPath, checksumPath, binaryName string) error { } actualChecksum := fmt.Sprintf("%x", hasher.Sum(nil)) + if actualChecksum == expectedChecksum { + fmt.Printf("checksum matched!\n") + } + if actualChecksum != expectedChecksum { return fmt.Errorf("checksum mismatch: expected %s, got %s", expectedChecksum, actualChecksum) } @@ -213,10 +231,6 @@ func verifyChecksum(binaryPath, checksumPath, binaryName string) error { return nil } -func replaceBinary(currentPath, newPath string) error { - return os.Rename(newPath, currentPath) -} - type progressWriter struct { io.Writer Total int64 From 0aab589343320ee1d6198222c423c00898d34314 Mon Sep 17 00:00:00 2001 From: Quinten <67589015+QuintenQVD0@users.noreply.github.com> Date: Fri, 27 Dec 2024 17:41:21 +0100 Subject: [PATCH 6/7] Simplify version logic --- cmd/selfupdate.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cmd/selfupdate.go b/cmd/selfupdate.go index e0b83f6..e058404 100644 --- a/cmd/selfupdate.go +++ b/cmd/selfupdate.go @@ -57,11 +57,9 @@ func selfupdateCmdRun(_ *cobra.Command, _ []string) { return } - var currentVersionTag string - if currentVersion != "develop" { - currentVersionTag = "v" + currentVersion - } else { - currentVersionTag = "develop" + currentVersionTag := "v" + currentVersion + if currentVersion == "develop" { + currentVersionTag = currentVersion } if latestVersionTag == currentVersionTag && !updateArgs.force { From b8845f91c30c43a1aa375e9b7c2e57d622dcad68 Mon Sep 17 00:00:00 2001 From: Quinten <67589015+QuintenQVD0@users.noreply.github.com> Date: Fri, 27 Dec 2024 17:51:57 +0100 Subject: [PATCH 7/7] Update: Checksum verification successful message --- cmd/selfupdate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/selfupdate.go b/cmd/selfupdate.go index e058404..aca23a2 100644 --- a/cmd/selfupdate.go +++ b/cmd/selfupdate.go @@ -219,7 +219,7 @@ func verifyChecksum(binaryPath, checksumPath, binaryName string) error { actualChecksum := fmt.Sprintf("%x", hasher.Sum(nil)) if actualChecksum == expectedChecksum { - fmt.Printf("checksum matched!\n") + fmt.Printf("Checksum verification successful!\n") } if actualChecksum != expectedChecksum {