diff --git a/cli/cmd/upgrade.go b/cli/cmd/upgrade.go new file mode 100644 index 0000000..8d352de --- /dev/null +++ b/cli/cmd/upgrade.go @@ -0,0 +1,280 @@ +package cmd + +import ( + "archive/tar" + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + + "github.com/pterm/pterm" + + "github.com/spf13/cobra" +) + +func fetchLatestVersion() ([]string, error) { + + logCli.Print(ctx, "Fetching available versions") + + type Release struct { + TagName string `json:"tag_name"` + } + + resp, err := http.Get("https://api.github.com/repos/ksctl/cli/releases") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var _releases []Release + if err := json.NewDecoder(resp.Body).Decode(&_releases); err != nil { + return nil, err + } + + var releases []string + rcRegex := regexp.MustCompile(`.*-rc[0-9]+$`) + for _, release := range _releases { + if rcRegex.MatchString(release.TagName) { + continue + } + releases = append(releases, release.TagName) + } + + return releases, nil +} + +func filterToUpgradeableVersions(versions []string) []string { + var upgradeableVersions []string + for _, version := range versions { + if version > Version { + upgradeableVersions = append(upgradeableVersions, version) + } + } + return upgradeableVersions +} + +func downloadFile(url, localFilename string) error { + logCli.Print(ctx, "Downloading file", "url", url, "localFilename", localFilename) + + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + out, err := os.Create(localFilename) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} +func verifyChecksum(filePath, checksumfileLoc string) (bool, error) { + logCli.Print(ctx, "Verifying checksum", "file", filePath, "checksumfile", checksumfileLoc) + + rawChecksum, err := os.ReadFile(checksumfileLoc) + if err != nil { + return false, err + } + checksums := strings.Split(string(rawChecksum), "\n") + + var expectedChecksum string = "LOL" + for _, line := range checksums { + if strings.Contains(line, filePath) { + expectedChecksum = strings.Fields(line)[0] + break + } + } + if expectedChecksum == "LOL" { + return false, logCli.NewError(ctx, "Checksum not found in checksum file") + } + + file, err := os.Open(filePath) + if err != nil { + return false, err + } + defer file.Close() + + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return false, err + } + + calculatedChecksum := hex.EncodeToString(hash.Sum(nil)) + return calculatedChecksum == expectedChecksum, nil +} + +func getOsArch() (string, error) { + arch := runtime.GOARCH + + if arch != "amd64" && arch != "arm64" { + return "", logCli.NewError(ctx, "Unsupported architecture") + } + return arch, nil +} + +func getOs() (string, error) { + os := runtime.GOOS + + if os != "linux" && os != "darwin" { + return "", logCli.NewError(ctx, "Unsupported OS", "message", "will provide support for windows based OS soon") + } + return os, nil +} + +func update(version string) error { + osName, err := getOs() + if err != nil { + return err + } + archName, err := getOsArch() + if err != nil { + return err + } + + logCli.Print(ctx, "Delected System", "OS", osName, "Arch", archName) + downloadURLBase := fmt.Sprintf("https://github.com/ksctl/cli/releases/download/%s", version) + tarFile := fmt.Sprintf("ksctl-cli_%s_%s_%s.tar.gz", version[1:], osName, archName) + checksumFile := fmt.Sprintf("ksctl-cli_%s_checksums.txt", version[1:]) + + tarUri := fmt.Sprintf("%s/%s", downloadURLBase, tarFile) + checksumUri := fmt.Sprintf("%s/%s", downloadURLBase, checksumFile) + + defer func() { + logCli.Print(ctx, "Cleaning up") + if err := os.Remove(checksumFile); err != nil { + logCli.Error("Failed to remove checksum file", "error", err) + } + + if err := os.Remove(tarFile); err != nil { + logCli.Error("Failed to remove checksum file", "error", err) + } + }() + + if err := downloadFile(tarUri, tarFile); err != nil { + return err + } + + if err := downloadFile(checksumUri, checksumFile); err != nil { + return err + } + + match, err := verifyChecksum(tarFile, checksumFile) + if err != nil { + return logCli.NewError(ctx, "Failed to verify checksum", "error", err) + } + if !match { + return logCli.NewError(ctx, "Checksum verification failed") + } + logCli.Success(ctx, "Checksum verification successful") + + tempDir, err := os.MkdirTemp("", "ksctl-update") + if err != nil { + return logCli.NewError(ctx, "Failed to create temp dir", "error", err) + } + file, err := os.Open(tarFile) + if err != nil { + return logCli.NewError(ctx, "Failed to open tar file", "error", err) + } + defer file.Close() + + gzr, err := gzip.NewReader(file) + if err != nil { + return logCli.NewError(ctx, "Failed to read gzip file", "error", err) + } + defer gzr.Close() + tr := tar.NewReader(gzr) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return logCli.NewError(ctx, "Failed to read tar file", "error", err) + } + if header.Name == "ksctl" { + outFile, err := os.Create(filepath.Join(tempDir, "ksctl")) + if err != nil { + return logCli.NewError(ctx, "Failed to create ksctl binary", "error", err) + } + defer outFile.Close() + + if _, err := io.Copy(outFile, tr); err != nil { + return logCli.NewError(ctx, "Failed to copy ksctl binary", "error", err) + } + break + } + } + + logCli.Print(ctx, "Making ksctl executable...") + if err := os.Chmod(filepath.Join(tempDir, "ksctl"), 0550); err != nil { + return logCli.NewError(ctx, "Failed to make ksctl executable", "error", err) + } + + logCli.Print(ctx, "Moving ksctl to /usr/local/bin (requires sudo)...") + cmd := exec.Command("sudo", "mv", "-v", filepath.Join(tempDir, "ksctl"), "/usr/local/bin/ksctl") + err = cmd.Run() + if err != nil { + return logCli.NewError(ctx, "Failed to move ksctl to /usr/local/bin", "error", err) + } + + _, err = exec.LookPath("ksctl") + if err != nil { + return logCli.NewError(ctx, "Failed to find ksctl in PATH", "error", err) + } + + return nil +} + +var selfUpdate = &cobra.Command{ + Use: "self-update", + Short: "update the ksctl cli", + Long: "setups up update for ksctl cli", + Run: func(cmd *cobra.Command, args []string) { + + if Version == "dev" { + logCli.Error("Cannot update dev version", "msg", "Please use a stable version to update") + os.Exit(1) + } + + logCli.Warn(ctx, "Currently no migrations are supported", "msg", "Please help us by creating a PR to support migrations. Thank you!") + + vers, err := fetchLatestVersion() + if err != nil { + logCli.Error("Failed to fetch latest version", "error", err) + os.Exit(1) + } + vers = filterToUpgradeableVersions(vers) + + logCli.Print(ctx, "Available versions to update") + selectedOption, _ := pterm.DefaultInteractiveSelect.WithOptions(vers).Show() + + newVer := selectedOption + + if err := update(newVer); err != nil { + logCli.Error("Failed to update ksctl cli", "error", err) + os.Exit(1) + } + + logCli.Success(ctx, "Updated Ksctl cli", "previousVer", Version, "newVer", newVer) + logCli.Note(ctx, "Please restart your terminal to use the updated version") + }, +} + +func init() { + RootCmd.AddCommand(selfUpdate) + storageFlag(selfUpdate) + + selfUpdate.Flags().BoolP("verbose", "v", true, "for verbose output") +} diff --git a/cli/cmd/version.go b/cli/cmd/version.go index 8896e96..5875aaf 100644 --- a/cli/cmd/version.go +++ b/cli/cmd/version.go @@ -17,15 +17,6 @@ const ( | <\__ \ (__| |_| | |_|\_\___/\___|\__|_| -` - - v1_0Ksctl = ` - __ __ .__ -| | __ ______ ____ _/ |_ | | -| |/ / / ___/_/ ___\\ __\| | -| < \___ \ \ \___ | | | |__ -|__|_ \/____ > \___ >|__| |____/ - \/ \/ \/ ` v2_0Ksctl = ` @@ -48,9 +39,7 @@ var versionCmd = &cobra.Command{ Short: "Print the version number of ksctl", Run: func(cmd *cobra.Command, args []string) { - fmt.Println(v0_1Ksctl) - - color.HiGreen(v1_0Ksctl) + color.HiGreen(v0_1Ksctl) x := strings.Split(v2_0Ksctl, "\n") diff --git a/logger/general_logging.go b/logger/general_logging.go index fcd9f00..0957c42 100644 --- a/logger/general_logging.go +++ b/logger/general_logging.go @@ -193,7 +193,10 @@ func (l *GeneralLog) Table(ctx context.Context, op consts.LogClusterDetail, data ) } + println() tbl.Print() + println() + println() } else if op == consts.LoggingInfoCluster { a, err := json.MarshalIndent(data[0], "", " ") if err != nil {