Scout/cmd/scout-client/update_commands.go
2026-08-31 15:47:55 +08:00

304 lines
12 KiB
Go

package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"scout/pkg/client"
"scout/pkg/scoutpkg"
"scout/pkg/updater"
)
type targetFlags struct {
platform *string
architecture *string
channel *string
}
type managerFlags struct {
server *string
packageName *string
rootDir *string
currentVersion *string
currentPath *string
entrypoint *string
platform *string
architecture *string
channel *string
healthTimeout *time.Duration
keepReleases *int
}
func checkCommand(args []string) {
flags := flag.NewFlagSet("check", flag.ExitOnError)
server := flags.String("server", "http://localhost:8080", "\u670d\u52a1\u7aef\u5730\u5740")
name := flags.String("package", "", "\u5305\u540d")
current := flags.String("current", "", "\u5f53\u524d\u7248\u672c")
target := bindTargetFlags(flags)
flags.Parse(args)
requirePackage(*name)
api := newClient(*server, "")
latest, available, err := api.CheckUpdateFor(context.Background(), *name, *current, target.options(*current))
if err != nil {
fatal("\u68c0\u67e5\u66f4\u65b0\u5931\u8d25: %v", err)
}
if available {
fmt.Printf("\u53d1\u73b0\u66f4\u65b0\uff1a%s %s -> %s\uff08%s/%s\uff09\n", latest.Name, *current, latest.Version, latest.Platform, latest.Architecture)
return
}
fmt.Printf("\u5f53\u524d\u5df2\u662f\u6700\u65b0\u7248\u672c\uff1a%s\n", latest.Version)
}
func downloadCommand(args []string) {
flags := flag.NewFlagSet("download", flag.ExitOnError)
server := flags.String("server", "http://localhost:8080", "\u670d\u52a1\u7aef\u5730\u5740")
name := flags.String("package", "", "\u5305\u540d")
version := flags.String("version", "", "\u7248\u672c\uff1b\u4e3a\u7a7a\u65f6\u4e0b\u8f7d\u6700\u65b0\u7248\u672c")
current := flags.String("current", "", "\u5f53\u524d\u7248\u672c")
output := flags.String("output", "", "\u4fdd\u5b58\u8def\u5f84")
target := bindTargetFlags(flags)
flags.Parse(args)
requirePackage(*name)
if *output == "" {
resolvedVersion := *version
if resolvedVersion == "" {
resolvedVersion = "latest"
}
*output = filepath.Join(".", fmt.Sprintf("%s-%s.pkg", *name, resolvedVersion))
}
api := newClient(*server, "")
item, err := api.DownloadFor(context.Background(), *name, *version, *output, target.options(*current))
if err != nil {
fatal("\u4e0b\u8f7d\u5931\u8d25: %v", err)
}
fmt.Printf("\u4e0b\u8f7d\u5b8c\u6210\uff1a%s\n\u7248\u672c\uff1a%s\n\u76ee\u6807\uff1a%s/%s\nSHA-256\uff1a%s\n", *output, item.Version, item.Platform, item.Architecture, item.SHA256)
}
func uploadCommand(args []string) {
flags := flag.NewFlagSet("upload", flag.ExitOnError)
server := flags.String("server", "http://localhost:8080", "\u670d\u52a1\u7aef\u5730\u5740")
token := flags.String("token", "", "\u5f00\u53d1\u8005\u4ee4\u724c")
name := flags.String("package", "", "\u5305\u540d\uff1bScout \u5305\u53ef\u7701\u7565")
version := flags.String("version", "", "\u7248\u672c\uff1bScout \u5305\u53ef\u7701\u7565")
filePath := flags.String("file", "", "\u5305\u6587\u4ef6\u8def\u5f84")
description := flags.String("description", "", "\u5305\u63cf\u8ff0")
packageType := flags.String("package-type", "full", "\u5305\u7c7b\u578b\uff1afull \u6216 delta")
channel := flags.String("channel", "stable", "\u53d1\u5e03\u901a\u9053\uff1astable\u3001beta \u6216 dev")
status := flags.String("status", "published", "\u53d1\u5e03\u72b6\u6001")
upgradeFrom := flags.String("upgrade-from", "", "\u5dee\u5206\u5305\u6765\u6e90\u7248\u672c\uff0c\u591a\u4e2a\u7528\u9017\u53f7\u5206\u9694")
target := bindTargetFlags(flags)
flags.Parse(args)
if *filePath == "" {
fatal("\u5fc5\u987b\u6307\u5b9a --file")
}
isScout := strings.EqualFold(filepath.Ext(*filePath), ".scout")
if !isScout && (*name == "" || *version == "" || *target.platform == "" || *target.architecture == "") {
fatal("\u666e\u901a\u5305\u5fc5\u987b\u6307\u5b9a --package\u3001--version\u3001--platform \u548c --architecture")
}
packageTypeValue := strings.TrimSpace(*packageType)
channelValue := strings.TrimSpace(*channel)
statusValue := strings.TrimSpace(*status)
if !isScout {
if packageTypeValue == "" {
packageTypeValue = "full"
}
if channelValue == "" {
channelValue = "stable"
}
if statusValue == "" {
statusValue = "published"
}
}
file, err := os.Open(*filePath)
if err != nil {
fatal("\u6253\u5f00\u5305\u6587\u4ef6\u5931\u8d25: %v", err)
}
defer file.Close()
api := newClient(*server, *token)
item, err := api.Upload(context.Background(), client.UploadRequest{
Name: *name,
Version: *version,
Description: *description,
Platform: *target.platform,
Architecture: *target.architecture,
PackageType: packageTypeValue,
Channel: channelValue,
Status: statusValue,
UpgradeFrom: splitPackages(*upgradeFrom),
FileName: filepath.Base(*filePath),
File: file,
})
if err != nil {
fatal("\u4e0a\u4f20\u5931\u8d25: %v", err)
}
fmt.Printf("\u4e0a\u4f20\u6210\u529f\uff1a%s %s\n\u901a\u9053\uff1a%s\n\u76ee\u6807\u6570\uff1a%d\nSHA-256\uff1a%s\n", item.Name, item.Version, item.Channel, len(item.Targets), item.SHA256)
}
func watchCommand(args []string) {
flags := flag.NewFlagSet("watch", flag.ExitOnError)
server := flags.String("server", "http://localhost:8080", "\u670d\u52a1\u7aef\u5730\u5740")
packages := flags.String("packages", "", "\u8981\u76d1\u542c\u7684\u5305\u540d\uff0c\u591a\u4e2a\u5305\u7528\u9017\u53f7\u5206\u9694")
versions := flags.String("versions", "", "\u5f53\u524d\u7248\u672c\uff0c\u683c\u5f0f\u4e3a package:version\uff0c\u591a\u4e2a\u7528\u9017\u53f7\u5206\u9694")
target := bindTargetFlags(flags)
flags.Parse(args)
api := newClient(*server, "")
updates, err := api.WatchUpdatesFor(context.Background(), client.WatchRequest{
Packages: splitPackages(*packages),
Versions: parseVersions(*versions),
Platform: *target.platform,
Architecture: *target.architecture,
Channel: *target.channel,
})
if err != nil {
fatal("\u8fde\u63a5\u66f4\u65b0\u901a\u77e5\u5931\u8d25: %v", err)
}
fmt.Println("\u5df2\u8fde\u63a5\u66f4\u65b0\u901a\u77e5\uff0c\u7b49\u5f85\u65b0\u5305\u2026\u2026")
for update := range updates {
fmt.Printf("\u6536\u5230\u66f4\u65b0\uff1a%s %s\uff08%s/%s\uff09\n", update.Package.Name, update.Package.Version, update.Package.Platform, update.Package.Architecture)
}
}
func prepareCommand(args []string) {
flags := flag.NewFlagSet("prepare", flag.ExitOnError)
managerInput := bindManagerFlags(flags)
version := flags.String("version", "", "\u8981\u51c6\u5907\u7684\u7248\u672c\uff1b\u4e3a\u7a7a\u65f6\u4f7f\u7528\u6700\u65b0\u7248\u672c")
flags.Parse(args)
manager := newUpdateManager(managerInput)
prepared, err := manager.Prepare(context.Background(), *version)
if err != nil {
fatal("\u51c6\u5907\u66f4\u65b0\u5931\u8d25: %v", err)
}
fmt.Printf("\u66f4\u65b0\u5df2\u51c6\u5907\uff1a%s\n\u76ee\u5f55\uff1a%s\n\u5c1a\u672a\u5207\u6362\u5f53\u524d\u7a0b\u5e8f\uff0c\u53ef\u7531\u7528\u6237\u51b3\u5b9a\u4f55\u65f6\u91cd\u542f\u3002\n", prepared.Version, prepared.Path)
}
func statusCommand(args []string) {
flags := flag.NewFlagSet("status", flag.ExitOnError)
managerInput := bindManagerFlags(flags)
flags.Parse(args)
status, err := newUpdateManager(managerInput).Status()
if err != nil {
fatal("\u8bfb\u53d6\u66f4\u65b0\u72b6\u6001\u5931\u8d25: %v", err)
}
data, _ := json.MarshalIndent(status, "", " ")
fmt.Println(string(data))
}
func activateCommand(args []string) {
flags := flag.NewFlagSet("activate", flag.ExitOnError)
managerInput := bindManagerFlags(flags)
restartNow := flags.Bool("restart-now", false, "\u6fc0\u6d3b\u540e\u4f7f\u7528\u7ea6\u5b9a\u9000\u51fa\u7801\u901a\u77e5\u5bbf\u4e3b\u7acb\u5373\u91cd\u542f")
flags.Parse(args)
activated, err := newUpdateManager(managerInput).Activate()
if err != nil {
fatal("\u6fc0\u6d3b\u66f4\u65b0\u5931\u8d25: %v", err)
}
fmt.Printf("\u5df2\u6fc0\u6d3b\u7248\u672c\uff1a%s\n", activated.Version)
if *restartNow {
os.Exit(updater.ExitRestartNow)
}
}
func healthyCommand(args []string) {
flags := flag.NewFlagSet("healthy", flag.ExitOnError)
managerInput := bindManagerFlags(flags)
version := flags.String("version", "", "\u8981\u786e\u8ba4\u7684\u5f53\u524d\u7248\u672c")
flags.Parse(args)
if err := newUpdateManager(managerInput).ConfirmHealthy(*version); err != nil {
fatal("\u5065\u5eb7\u786e\u8ba4\u5931\u8d25: %v", err)
}
fmt.Println("\u65b0\u7248\u672c\u5df2\u901a\u8fc7\u5065\u5eb7\u786e\u8ba4\uff0c\u65e7\u7248\u672c\u5df2\u6309\u4fdd\u7559\u7b56\u7565\u6e05\u7406\u3002")
}
func rollbackCommand(args []string) {
flags := flag.NewFlagSet("rollback", flag.ExitOnError)
managerInput := bindManagerFlags(flags)
reason := flags.String("reason", "\u7528\u6237\u624b\u52a8\u56de\u6eda", "\u56de\u6eda\u539f\u56e0")
flags.Parse(args)
rolledBack, err := newUpdateManager(managerInput).Rollback(fmt.Errorf("%s", *reason))
if err != nil {
fatal("\u56de\u6eda\u5931\u8d25: %v", err)
}
fmt.Printf("\u5df2\u56de\u6eda\u5230\u7248\u672c\uff1a%s\n", rolledBack.Version)
}
func bindTargetFlags(flags *flag.FlagSet) targetFlags {
defaultPlatform, defaultArchitecture := scoutpkg.CurrentTarget()
return targetFlags{
platform: flags.String("platform", defaultPlatform, "\u8fd0\u884c\u5e73\u53f0"),
architecture: flags.String("architecture", defaultArchitecture, "\u8fd0\u884c\u67b6\u6784"),
channel: flags.String("channel", "stable", "\u53d1\u5e03\u901a\u9053"),
}
}
func (target targetFlags) options(currentVersion string) client.QueryOptions {
return client.QueryOptions{
Platform: *target.platform,
Architecture: *target.architecture,
Channel: *target.channel,
CurrentVersion: currentVersion,
}
}
func bindManagerFlags(flags *flag.FlagSet) managerFlags {
defaultPlatform, defaultArchitecture := scoutpkg.CurrentTarget()
return managerFlags{
server: flags.String("server", "http://localhost:8080", "\u670d\u52a1\u7aef\u5730\u5740"),
packageName: flags.String("package", "", "\u5305\u540d"),
rootDir: flags.String("root", ".scout-update", "\u66f4\u65b0\u5de5\u4f5c\u76ee\u5f55"),
currentVersion: flags.String("current", "", "\u5f53\u524d\u7248\u672c"),
currentPath: flags.String("current-path", "", "\u5f53\u524d\u7a0b\u5e8f\u76ee\u5f55"),
entrypoint: flags.String("entrypoint", "", "\u7a0b\u5e8f\u5165\u53e3\u76f8\u5bf9\u8def\u5f84"),
platform: flags.String("platform", defaultPlatform, "\u8fd0\u884c\u5e73\u53f0"),
architecture: flags.String("architecture", defaultArchitecture, "\u8fd0\u884c\u67b6\u6784"),
channel: flags.String("channel", "stable", "\u53d1\u5e03\u901a\u9053"),
healthTimeout: flags.Duration("health-timeout", 2*time.Minute, "\u5065\u5eb7\u786e\u8ba4\u8d85\u65f6\u65f6\u95f4"),
keepReleases: flags.Int("keep", 2, "\u5065\u5eb7\u786e\u8ba4\u540e\u4fdd\u7559\u7684\u7248\u672c\u6570"),
}
}
func newUpdateManager(input managerFlags) *updater.Manager {
requirePackage(*input.packageName)
api := newClient(*input.server, "")
manager, err := updater.New(updater.Config{
Client: api,
PackageName: *input.packageName,
CurrentVersion: *input.currentVersion,
CurrentPath: *input.currentPath,
RootDir: *input.rootDir,
Platform: *input.platform,
Architecture: *input.architecture,
Channel: *input.channel,
Entrypoint: *input.entrypoint,
HealthTimeout: *input.healthTimeout,
KeepReleases: *input.keepReleases,
})
if err != nil {
fatal("\u521b\u5efa\u66f4\u65b0\u7ba1\u7406\u5668\u5931\u8d25: %v", err)
}
return manager
}
func parseVersions(value string) map[string]string {
result := make(map[string]string)
for _, entry := range splitPackages(value) {
parts := strings.SplitN(entry, ":", 2)
if len(parts) == 2 && strings.TrimSpace(parts[0]) != "" {
result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
}
return result
}
func printUsage() {
fmt.Println("Scout \u8f7b\u91cf\u5316\u5305\u7ba1\u7406\u5ba2\u6237\u7aef")
fmt.Println("\u7528\u6cd5\uff1ascout-client <check|download|upload|watch|prepare|status|activate|healthy|rollback> [\u53c2\u6570]")
fmt.Println("check\u3001download\u3001watch \u7528\u4e8e\u66f4\u65b0\u67e5\u8be2\uff0cupload \u7528\u4e8e\u5f00\u53d1\u8005\u4e0a\u4f20\u3002")
fmt.Println("prepare \u53ea\u51c6\u5907\u65b0\u7248\u672c\uff0cactivate \u7531\u7528\u6237\u51b3\u5b9a\u91cd\u542f\u65f6\u673a\uff0chealthy \u786e\u8ba4\u6210\u529f\u540e\u624d\u6e05\u7406\u65e7\u7248\u672c\u3002")
}