cubebox

package
v0.0.0-...-c9906ea Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: Apache-2.0, BSD-2-Clause Imports: 40 Imported by: 0

Documentation

Overview

Package cubebox provides a CLI for managing cubebox.

Index

Constants

This section is empty.

Variables

View Source
var Command = cli.Command{
	Name:    "cubebox",
	Aliases: []string{"cubebox"},
	Usage:   "manage cubebox",
	Subcommands: cli.Commands{
		MultiRun,
		ListCommand,
		InfoCommand,
		DestroyCommand,
		RollbackCommand,
		SandboxCommand,
		NodeCommand,
		SnapshotCommand,
		StorageCommand,
		OperationCommand,
		TemplateCommand,
		VolumeCommand,
	},
}
View Source
var DestroyCommand = cli.Command{
	Name:      "destroy",
	Aliases:   []string{"rm"},
	Usage:     "destroy sandbox instances",
	ArgsUsage: "<sandbox-id> [sandbox-id ...]",
	Action: func(c *cli.Context) error {
		if c.NArg() == 0 {
			_ = cli.ShowCommandHelp(c, "destroy")
			return errors.New("sandbox id is required")
		}

		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			log.Printf("no server addr")
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")

		var rmErr error
		for _, sandboxID := range c.Args() {
			err := doInnerDestroySandbox(c, sandboxID, map[string]string{constants.Caller: "mastercli"}, "")
			if err != nil {
				log.Printf("destroy failed: %s %s\n", sandboxID, err.Error())
				rmErr = errors.Join(rmErr, fmt.Errorf("%s: %w", sandboxID, err))
				continue
			}
			log.Printf("destroyed: %s\n", sandboxID)
		}

		return rmErr
	},
}
View Source
var InfoCommand = cli.Command{
	Name:  "info",
	Usage: "info sandboxes",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "sandboxid,s",
			Usage: "Either sandboxid or hostid; both can be specified, though specifying both has little effect since sandboxid determines the host",
		},
		cli.StringFlag{
			Name:  "hostid,t",
			Usage: "Either hostid or sandboxid; both can be specified to query a specific instance",
		},
		cli.BoolFlag{
			Name:  "old",
			Usage: "/cube/sandbox/info legacy API usage",
		},
		cli.IntFlag{
			Name:  "containerport,p",
			Usage: "Optional, target container port when querying exposed port",
		},
		cli.StringFlag{
			Name:  "callerhostip",
			Usage: "Optional, simulate the HostIP of the cube proxy node to select tap ip or host port",
		},
	},
	Action: func(c *cli.Context) error {
		hostID := c.String("hostid")
		sandboxID := c.String("sandboxid")
		if hostID == "" && sandboxID == "" {
			return errors.New("hostid and sandboxid cannot both be empty")
		}

		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			log.Printf("no server addr\n")
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")

		requestID := uuid.New().String()
		host := serverList[rand.Int()%len(serverList)]
		containerPort := c.Int("containerport")

		url := ""
		var body io.Reader
		if c.Bool("old") {

			req := &types.GetCubeSandboxReq{
				RequestID:     requestID,
				SandboxID:     sandboxID,
				HostID:        hostID,
				ContainerPort: int32(containerPort),
			}
			reqEn, _ := jsoniter.Marshal(req)
			body = bytes.NewBuffer(reqEn)
			url = fmt.Sprintf("http://%s/cube/sandbox/info", net.JoinHostPort(host, port))
		} else {
			if hostID != "" {
				url = fmt.Sprintf("http://%s/cube/sandbox/info?requestID=%s&host_id=%s", net.JoinHostPort(host, port), requestID, hostID)
			} else {
				url = fmt.Sprintf("http://%s/cube/sandbox/info?requestID=%s&sandbox_id=%s", net.JoinHostPort(host, port), requestID, sandboxID)
			}
			if containerPort > 0 {
				url = fmt.Sprintf("%s&container_port=%d", url, containerPort)
			}
		}

		rsp := &types.GetCubeSandboxRes{}
		err := doHttpReq(c, url, http.MethodGet, requestID, body, rsp)
		if err != nil {
			log.Printf("list_getBodyData err. %s. RequestId: %s\n", err.Error(), requestID)
			return err
		}
		if rsp.Ret.RetCode != 200 {
			log.Printf("rsp err. %s. RequestId: %s\n", rsp.Ret.RetMsg, requestID)
			return errors.New(rsp.Ret.RetMsg)
		}

		w := tabwriter.NewWriter(os.Stdout, 4, 8, 4, ' ', 0)
		for idx, sb := range rsp.Data {
			if idx > 0 {
				fmt.Fprintln(w)
			}
			printSandboxInfoBlock(w, sb)
		}
		return w.Flush()
	},
}
View Source
var ListCommand = cli.Command{
	Name:    "list",
	Aliases: []string{"ls"},
	Usage:   "list sandboxes",
	Flags: []cli.Flag{
		cli.IntFlag{
			Name:  "index,i",
			Value: 1,
			Usage: "Used with size; starting position of cube host list (sorted by primary key id in db), starts from 1, mutually exclusive with hostid",
		},
		cli.IntFlag{
			Name:  "size,s",
			Value: 1,
			Usage: "Used with index; number of hosts to traverse in this request, mutually exclusive with hostid",
		},
		cli.StringFlag{
			Name:  "hostid,t",
			Usage: "Required when (index,size) is not specified; mutually exclusive with (index,size)",
		},
		cli.BoolFlag{
			Name:  "old",
			Usage: "/cube/sandbox/info legacy API usage",
		},
		cli.StringSliceFlag{
			Name:  "filter",
			Usage: "Filter conditions, multiple supported, format: key=value,key=value,key=value",
		},
		cli.StringFlag{
			Name:  "type",
			Value: "cubebox",
			Usage: "instancetype,cubebox",
		},
		cli.BoolFlag{
			Name:  "delete",
			Usage: "Whether to delete, must be used with hostid",
		},
		cli.BoolFlag{
			Name:  "quiet, q",
			Usage: "print only the container id",
		},
		cli.BoolFlag{
			Name:  "wide,w",
			Usage: "display more detailed info",
		},
		cli.BoolFlag{
			Name:  "all",
			Usage: "scan all healthy nodes across the cluster",
		},
		cli.StringFlag{
			Name:  "sandboxid",
			Usage: "sandbox id",
		},
	},
	Action: func(c *cli.Context) error {
		hostID := c.String("hostid")
		startIdx := c.Int("index")
		size := c.Int("size")
		all := c.Bool("all")

		if all && hostID != "" {
			return errors.New("all flag cannot be used with hostid")
		}

		if hostID == "" && (startIdx == 0 || size == 0) {
			return errors.New("at least one of hostid or (start_idx, size) must be provided")
		}

		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			log.Printf("no server addr\n")
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")

		requestID := uuid.New().String()
		host := serverList[rand.Int()%len(serverList)]

		quiet := c.Bool("quiet")
		delete := c.Bool("delete")
		sandboxID := c.String("sandboxid")
		if delete && (hostID == "" || !quiet) {
			return errors.New("delete flag must be used with hostid and quiet flag")
		}
		if delete && all {
			return errors.New("delete flag cannot be used with all flag")
		}

		filters, filterList := parseListFilters(c.StringSlice("filter"))
		req := &types.ListCubeSandboxReq{
			RequestID:    requestID,
			HostID:       hostID,
			StartIdx:     startIdx,
			Size:         size,
			InstanceType: c.String("type"),
		}
		if len(filters) > 0 {
			req.Filter = &types.CubeSandboxFilter{
				LabelSelector: filters,
			}
		}

		rsp, summary, err := runListQuery(c, host, req, filterList, all)
		if err != nil {
			log.Printf("list_getBodyData err. %s. RequestId: %s\n", err.Error(), requestID)
			return err
		}
		if rsp.Ret == nil {
			return errors.New("empty response")
		}
		if rsp.Ret.RetCode != 200 {
			log.Printf("rsp err. %s. RequestId: %s\n", rsp.Ret.RetMsg, requestID)
			return errors.New(rsp.Ret.RetMsg)
		}

		if quiet {
			var resolvedSandboxID string
			if sandboxID != "" {
				candidates := make([]string, 0, len(rsp.Data))
				for _, sandbox := range rsp.Data {
					candidates = append(candidates, sandbox.SandboxID)
				}
				var err error
				resolvedSandboxID, err = sandboxid.Resolve(sandboxID, candidates)
				if err != nil {
					return err
				}
			}
			for _, sandbox := range rsp.Data {
				if delete {
					if resolvedSandboxID != "" && sandbox.SandboxID != resolvedSandboxID {
						continue
					}
					err = doInnerDestroySandbox(c, sandbox.SandboxID, sandbox.Labels, c.String("type"))
					if err != nil {
						log.Printf("doDestroySandbox err. %s. RequestId: %s\n", err.Error(), requestID)
					}
					log.Printf("doDestroySandbox success: %s\n", sandbox.SandboxID)
				} else {
					fmt.Println(sandbox.SandboxID)
				}
			}
			return nil
		}

		w := tabwriter.NewWriter(os.Stdout, 4, 8, 4, ' ', 0)
		fmt.Fprintf(w, "NODE_SCOPE\t%s\n", summary.NodeScope)
		fmt.Fprintf(w, "NODES_SCANNED\t%d/%d\n", summary.NodesScanned, summary.NodesTotal)
		fmt.Fprintf(w, "SANDBOX_COUNT\t%d\n", summary.SandboxCount)
		fmt.Fprintln(w)
		tabHeader := "sandbox_id\tstatus\thost_id\tcreate_at\tpause_at"
		if c.Bool("wide") {
			tabHeader += "\ttemplate_id\tnamespace\thost_ip\tlabels"
		}
		sort.Slice(rsp.Data, func(i, j int) bool {
			return rsp.Data[i].CreateAt < rsp.Data[j].CreateAt
		})
		fmt.Fprintln(w, tabHeader)
		for _, sandbox := range rsp.Data {
			row := fmt.Sprintf("%s\t%s\t%s\t%s\t%s", sandbox.SandboxID,
				getStatus(sandbox.Status), sandbox.HostID, formatTime(sandbox.CreateAt), formatTime(sandbox.PauseAt))
			if c.Bool("wide") {
				row += fmt.Sprintf("\t%s\t%s\t%s\t%s", sandbox.TemplateID, sandbox.NameSpace, sandbox.HostIP,
					utils.InterfaceToString(sandbox.Labels))
			}
			if _, err := fmt.Fprintln(w, row); err != nil {
				return err
			}
		}
		return w.Flush()
	},
}
View Source
var ListInventoryCommand = cli.Command{
	Name:    "listinventory",
	Usage:   "list inventory",
	Aliases: []string{"li"},
	Flags: []cli.Flag{
		cli.StringSliceFlag{
			Name:  "filter",
			Usage: "Filter conditions, multiple supported, format: key=value,key=value,key=value",
		},
		cli.StringFlag{
			Name:  "type",
			Value: "cubebox",
			Usage: "instancetype,cubebox",
		},
	},
	Action: func(c *cli.Context) error {
		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			log.Printf("no server addr\n")
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")

		requestID := uuid.New().String()
		host := serverList[rand.Int()%len(serverList)]

		filterlist := c.StringSlice("filter")
		filters := make(map[string][]string)
		for _, filter := range filterlist {
			labels := strings.TrimSpace(filter)
			if labels == "" {
				continue
			}
			kv := strings.Split(labels, "=")
			if len(kv) >= 2 {
				filters[kv[0]] = append(filters[kv[0]], kv[1])
			}
		}

		req := &types.ListInventoryReq{
			RequestID:    requestID,
			InstanceType: c.String("type"),
		}
		if len(filters) > 0 {
			for k, v := range filters {
				req.Filters = append(req.Filters, &types.FilterItem{
					Name:   k,
					Values: v,
				})
			}
		}
		reqEn, _ := jsoniter.Marshal(req)
		url := fmt.Sprintf("http://%s/cube/listinventory", net.JoinHostPort(host, port))
		rsp := &types.ListInventoryRes{}
		err := doHttpReq(c, url, http.MethodPost, requestID, bytes.NewBuffer(reqEn), rsp)
		if err != nil {
			log.Printf("list_getBodyData err. %s. RequestId: %s\n", err.Error(), requestID)
			return err
		}
		if rsp.Ret.RetCode != 200 {
			log.Printf("rsp err. %s. RequestId: %s\n", rsp.Ret.RetMsg, requestID)
			return errors.New(rsp.Ret.RetMsg)
		}

		w := tabwriter.NewWriter(os.Stdout, 4, 8, 4, ' ', 0)
		fmt.Fprintln(w, "Zone\tCpuType\tCPU\tMemory")
		for _, res := range rsp.Data {
			fmt.Fprintf(w, "%s\t%s\t%d\t%d\n", res.Zone, res.CPUType, res.CPU, res.Memory)
		}

		return w.Flush()
	},
}
View Source
var MultiRun = cli.Command{
	Name:      "multirun",
	Usage:     "run one or more containers",
	ArgsUsage: "[flags] req.json1 [req.json2, ...]",
	Action:    multiRunAction,
	Flags: []cli.Flag{
		&cli.Int64Flag{
			Name:  "runcnt",
			Value: 1,
			Usage: "loop every req count",
		},
		&cli.IntFlag{
			Name:  "runcc",
			Value: 1,
			Usage: "concurrency of every req.json",
		},
		&cli.BoolFlag{
			Name:  "norm",
			Usage: "bench not remove",
		},
		&cli.BoolFlag{
			Name:  "printall",
			Usage: "print all result metric",
		},
		&cli.StringFlag{
			Name:  "percents",
			Value: "0.05,0.5,0.8,0.99",
			Usage: "Percentiles param",
		},
		&cli.BoolFlag{
			Name:  "fail_exit",
			Usage: "exit cli when create failure",
		},
		&cli.DurationFlag{
			Name:  "sleep_before_del",
			Value: 0,
			Usage: "sleep before delete container",
		},
		&cli.DurationFlag{
			Name:  "sleep_before_req",
			Value: 0,
			Usage: "sleep before req container",
		},
		&cli.BoolFlag{
			Name: "same",

			Usage: "Run with strict concurrent synchronization",
		},
		&cli.IntFlag{
			Name:  "delcc",
			Usage: "concurrency of destroy",
		},
		cli.StringFlag{
			Name:  "hostid,t",
			Usage: "Internal debug param; requires HTTP header to take effect. Specify physical machine ID to create containers",
		},
		cli.StringFlag{
			Name:  "hostip,s",
			Usage: "Internal debug param; requires HTTP header to take effect. Specify physical IP to create containers",
		},
		&cli.BoolFlag{
			Name:  "testmocksch",
			Usage: "test mock scheduling",
		},
		&cli.StringFlag{
			Name:  "biztype",
			Value: "all",
			Usage: "business type,default is all,[all/sh/shtcb/gz/bj]",
		},
		&cli.BoolFlag{
			Name:  "testmultireq",
			Usage: "test multi req json from file stub",
		},
		cli.StringFlag{
			Name:  "multireq_user_id",
			Value: "123456789",
			Usage: "user_id parameter for multiple functions",
		},
		&cli.IntFlag{
			Name:  "async_retry_max",
			Value: 100,
			Usage: "max retry count for async retry queue, 0 means give up immediately after sync retry failed",
		},
	},
}
View Source
var NodeCommand = cli.Command{
	Name:    "node",
	Aliases: []string{"nodes"},
	Usage:   "list / isolate / unisolate cubemaster nodes",
	Subcommands: cli.Commands{
		NodeListCommand,
		NodeIsolateCommand,
		NodeUnisolateCommand,
	},
}
View Source
var NodeIsolateCommand = cli.Command{
	Name:      "isolate",
	Usage:     "cordon node(s) (block new sandbox scheduling; existing sandboxes unaffected)",
	ArgsUsage: "<node-id> [node-id ...]",
	Flags:     nodeIsolationFlags,
	Action: func(c *cli.Context) error {
		return doNodeIsolation(c, http.MethodPut)
	},
}
View Source
var NodeListCommand = cli.Command{
	Name:    "list",
	Aliases: []string{"ls"},
	Usage:   "list node status from cubemaster internal endpoint",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "hostid",
			Usage: "query single host/node id",
		},
		cli.BoolFlag{
			Name:  "score-only",
			Usage: "only query score/update timestamps",
		},
		cli.BoolFlag{
			Name:  "json",
			Usage: "print raw json response",
		},
	},
	Action: func(c *cli.Context) error {
		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			log.Printf("no server addr\n")
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")
		requestID := uuid.New().String()
		host := serverList[rand.Int()%len(serverList)]

		url := fmt.Sprintf("http://%s/internal/node?requestID=%s", net.JoinHostPort(host, port), requestID)
		if hostID := c.String("hostid"); hostID != "" {
			url += "&host_id=" + hostID
		}
		if c.Bool("score-only") {
			url += "&score_only=true"
		}

		rsp := &nodeResponse{}
		if err := doHttpReq(c, url, http.MethodGet, requestID, nil, rsp); err != nil {
			log.Printf("node list request err. %s. RequestId: %s\n", err.Error(), requestID)
			return err
		}
		if rsp.Ret == nil {
			return errors.New("empty response")
		}
		if rsp.Ret.RetCode != 200 {
			log.Printf("node list failed. %s. RequestId: %s\n", rsp.Ret.RetMsg, requestID)
			return errors.New(rsp.Ret.RetMsg)
		}
		sort.Slice(rsp.Data, func(i, j int) bool {
			return rsp.Data[i].ID() < rsp.Data[j].ID()
		})
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		printNodeSummary(rsp.Data, c.Bool("score-only"))
		return nil
	},
}
View Source
var NodeUnisolateCommand = cli.Command{
	Name:      "unisolate",
	Usage:     "remove cordon so the node(s) can receive new sandboxes",
	ArgsUsage: "<node-id> [node-id ...]",
	Flags:     nodeIsolationFlags,
	Action: func(c *cli.Context) error {
		return doNodeIsolation(c, http.MethodDelete)
	},
}
View Source
var OperationCommand = cli.Command{
	Name:  "operation",
	Usage: "inspect snapshot operations",
	Subcommands: cli.Commands{
		OperationStatusCommand,
		OperationWatchCommand,
	},
}
View Source
var OperationStatusCommand = cli.Command{
	Name:  "status",
	Usage: "show snapshot operation status",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "operation-id", Usage: "snapshot operation id"},
		cli.BoolFlag{Name: "json", Usage: "print raw json response"},
	},
	Action: func(c *cli.Context) error {
		operationID := c.String("operation-id")
		if operationID == "" {
			return errors.New("operation-id is required")
		}
		rsp, err := fetchOperationStatus(c, operationID)
		if err != nil {
			return err
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		printOperationResponse(rsp.Operation)
		return nil
	},
}
View Source
var OperationWatchCommand = cli.Command{
	Name:  "watch",
	Usage: "watch snapshot operation until completion",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "operation-id", Usage: "snapshot operation id"},
		cli.DurationFlag{Name: "interval", Value: 2 * time.Second, Usage: "poll interval"},
		cli.BoolFlag{Name: "json", Usage: "print final raw json response"},
	},
	Action: func(c *cli.Context) error {
		operationID := c.String("operation-id")
		if operationID == "" {
			return errors.New("operation-id is required")
		}
		var lastPrinted string
		for {
			rsp, err := fetchOperationStatus(c, operationID)
			if err != nil {
				return err
			}
			current := ""
			if rsp.Operation != nil {
				current = fmt.Sprintf("%s/%d/%s", rsp.Operation.Status, rsp.Operation.Progress, rsp.Operation.Phase)
			}
			if current != lastPrinted {
				printOperationResponse(rsp.Operation)
				lastPrinted = current
			}
			if operationFinished(rsp.Operation) {
				if c.Bool("json") {
					commands.PrintAsJSON(rsp)
				}
				if rsp.Operation.Status == "FAILED" {
					return errors.New(rsp.Operation.ErrorMessage)
				}
				return nil
			}
			time.Sleep(c.Duration("interval"))
		}
	},
}
View Source
var RollbackCommand = cli.Command{
	Name:    "rollback",
	Aliases: []string{"sandbox-rollback"},
	Usage:   "rollback a sandbox to a snapshot",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "sandbox-id", Usage: "sandbox id to rollback"},
		cli.StringFlag{Name: "snapshot-id", Usage: "snapshot id to rollback to"},
		cli.StringFlag{Name: "instance-type", Usage: "instance type override"},
		cli.BoolFlag{Name: "json", Usage: "print raw json response"},
	},
	Action: func(c *cli.Context) error {
		sandboxID := c.String("sandbox-id")
		snapshotID := c.String("snapshot-id")
		if sandboxID == "" || snapshotID == "" {
			return errors.New("sandbox-id and snapshot-id are required")
		}
		requestID := uuid.NewString()
		req := &rollbackRequest{
			RequestID:    requestID,
			SandboxID:    sandboxID,
			SnapshotID:   snapshotID,
			InstanceType: c.String("instance-type"),
		}
		body, err := jsoniter.Marshal(req)
		if err != nil {
			return err
		}
		rsp := &operationResponse{}
		if err := doSnapshotReq(c, http.MethodPost, "/cube/sandbox/rollback", requestID, bytes.NewBuffer(body), rsp); err != nil {
			return err
		}
		if err := ensureSuccessRet(rsp.Ret); err != nil {
			return err
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		printOperationResponse(rsp.Operation)
		return nil
	},
}
View Source
var SandboxCommand = cli.Command{
	Name:  "sandbox",
	Usage: "sandbox operations",
	Subcommands: cli.Commands{
		RollbackCommand,
	},
}
View Source
var SnapshotCommand = cli.Command{
	Name:  "snapshot",
	Usage: "manage runtime snapshots",
	Subcommands: cli.Commands{
		SnapshotCreateCommand,
		SnapshotListCommand,
		SnapshotInfoCommand,
		SnapshotDeleteCommand,
	},
}
View Source
var SnapshotCreateCommand = cli.Command{
	Name:  "create",
	Usage: "create a snapshot from an existing sandbox",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "sandbox-id", Usage: "sandbox id to snapshot"},
		cli.StringFlag{Name: "display-name", Usage: "snapshot display name"},
		cli.BoolFlag{Name: "json", Usage: "print raw json response"},
	},
	Action: func(c *cli.Context) error {
		sandboxID := strings.TrimSpace(c.String("sandbox-id"))
		if sandboxID == "" {
			return errors.New("sandbox-id is required")
		}
		requestID := uuid.NewString()
		req := &snapshotCreateRequest{
			RequestID:   requestID,
			SandboxID:   sandboxID,
			DisplayName: c.String("display-name"),
		}
		body, err := jsoniter.Marshal(req)
		if err != nil {
			return err
		}
		rsp := &snapshotResponse{}
		if err := doSnapshotReq(c, http.MethodPost, "/cube/snapshot", requestID, bytes.NewBuffer(body), rsp); err != nil {
			return err
		}
		if err := ensureSuccessRet(rsp.Ret); err != nil {
			return err
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		printSnapshotResponse(rsp)
		return nil
	},
}
View Source
var SnapshotDeleteCommand = cli.Command{
	Name:  "delete",
	Usage: "delete snapshot metadata and node replicas",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "snapshot-id", Usage: "snapshot id to delete"},
		cli.StringFlag{Name: "instance-type", Usage: "instance type override"},
		cli.BoolFlag{Name: "json", Usage: "print raw json response"},
	},
	Action: func(c *cli.Context) error {
		snapshotID := c.String("snapshot-id")
		if snapshotID == "" {
			return errors.New("snapshot-id is required")
		}
		requestID := uuid.NewString()
		query := fmt.Sprintf("?request_id=%s", requestID)
		if instanceType := strings.TrimSpace(c.String("instance-type")); instanceType != "" {
			query += "&instance_type=" + instanceType
		}
		rsp := &operationResponse{}
		if err := doSnapshotReq(c, http.MethodDelete, "/cube/snapshot/"+snapshotID+query, requestID, nil, rsp); err != nil {
			return err
		}
		if err := ensureSuccessRet(rsp.Ret); err != nil {
			return err
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		printOperationResponse(rsp.Operation)
		return nil
	},
}
View Source
var SnapshotInfoCommand = cli.Command{
	Name:    "info",
	Aliases: []string{"describe"},
	Usage:   "show snapshot metadata and node replicas",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "snapshot-id", Usage: "snapshot id to query"},
		cli.BoolFlag{Name: "include-request", Usage: "include stored create request"},
		cli.BoolFlag{Name: "json", Usage: "print raw json response"},
	},
	Action: func(c *cli.Context) error {
		snapshotID := c.String("snapshot-id")
		if snapshotID == "" {
			return errors.New("snapshot-id is required")
		}
		requestID := uuid.NewString()
		endpoint := fmt.Sprintf("/cube/snapshot/%s", snapshotID)
		if c.Bool("include-request") {
			endpoint += "?include_request=true"
		}
		rsp := &snapshotResponse{}
		if err := doSnapshotReq(c, http.MethodGet, endpoint, requestID, nil, rsp); err != nil {
			return err
		}
		if err := ensureSuccessRet(rsp.Ret); err != nil {
			return err
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		printSnapshotResponse(rsp)
		return nil
	},
}
View Source
var SnapshotListCommand = cli.Command{
	Name:    "list",
	Aliases: []string{"ls"},
	Usage:   "list runtime snapshots",
	Flags: []cli.Flag{
		cli.BoolFlag{Name: "json", Usage: "print raw json response"},
		cli.StringFlag{Name: "output,o", Usage: "output format, set to wide for more columns"},
	},
	Action: func(c *cli.Context) error {
		rsp := &snapshotListResponse{}
		requestID := uuid.NewString()
		if err := doSnapshotReq(c, http.MethodGet, "/cube/snapshot", requestID, nil, rsp); err != nil {
			return err
		}
		if err := ensureSuccessRet(rsp.Ret); err != nil {
			return err
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		wideOutput := strings.EqualFold(strings.TrimSpace(c.String("output")), "wide")
		w := tabwriter.NewWriter(os.Stdout, 4, 8, 4, ' ', 0)
		header := "SNAPSHOT_ID\tSTATUS\tSANDBOX_ID\tNODE_ID\tCREATED_AT"
		if wideOutput {
			header = "SNAPSHOT_ID\tSTATUS\tDISPLAY_NAME\tSANDBOX_ID\tNODE_ID\tRUNTIME_REFS\tBACKEND\tLAST_ERROR"
		}
		fmt.Fprintln(w, header)
		for _, item := range rsp.Data {
			originNode := item.OriginNodeID
			if wideOutput {
				fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%d\t%s\t%s\n",
					item.SnapshotID, item.Status, item.DisplayName, item.OriginSandboxID, originNode, item.RuntimeRefCount, item.StorageBackend, item.LastError)
				continue
			}
			fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
				item.SnapshotID, item.Status, item.OriginSandboxID, originNode, item.CreatedAt)
		}
		return w.Flush()
	},
}
View Source
var StorageCommand = cli.Command{
	Name:  "storage",
	Usage: "inspect snapshot storage state",
	Subcommands: cli.Commands{
		StorageStatusCommand,
	},
}
View Source
var StorageStatusCommand = cli.Command{
	Name:  "status",
	Usage: "show aggregated snapshot storage status from cubemaster",
	Flags: []cli.Flag{
		cli.BoolFlag{Name: "refresh", Usage: "refresh node metrics before returning"},
		cli.BoolFlag{Name: "json", Usage: "print raw json response"},
	},
	Action: func(c *cli.Context) error {
		requestID := uuid.NewString()
		endpoint := "/cube/snapshot/storage"
		if c.Bool("refresh") {
			endpoint += "?refresh=true"
		}
		rsp := &snapshotStorageStatusResponse{}
		if err := doSnapshotReq(c, http.MethodGet, endpoint, requestID, nil, rsp); err != nil {
			return err
		}
		if err := ensureSuccessRet(rsp.Ret); err != nil {
			return err
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		printSnapshotStorageStatus(rsp.Data)
		return nil
	},
}
View Source
var TemplateBuildStatusCommand = cli.Command{
	Name:  "build-status",
	Usage: "show sandbox commit build status",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "build-id", Usage: "template build id"},
		cli.BoolFlag{Name: "json", Usage: "print raw json response"},
	},
	Action: func(c *cli.Context) error {
		buildID := c.String("build-id")
		if buildID == "" {
			return errors.New("build-id is required")
		}
		rsp, err := fetchTemplateBuildStatus(c, buildID)
		if err != nil {
			return err
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		printTemplateBuildStatus(rsp)
		return nil
	},
}
View Source
var TemplateBuildWatchCommand = cli.Command{
	Name:  "build-watch",
	Usage: "watch sandbox commit build status until completion",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "build-id", Usage: "template build id"},
		cli.DurationFlag{Name: "interval", Value: 2 * time.Second, Usage: "poll interval"},
		cli.BoolFlag{Name: "json", Usage: "print final raw json response"},
	},
	Action: func(c *cli.Context) error {
		buildID := c.String("build-id")
		if buildID == "" {
			return errors.New("build-id is required")
		}
		return runBuildWatch(c, buildID)
	},
}
View Source
var TemplateCommitCommand = cli.Command{
	Name:  "commit",
	Usage: "commit an existing sandbox into a template",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "sandbox-id",
			Usage: "sandbox id to commit",
		},
		cli.StringFlag{
			Name:  "file, f",
			Usage: "original create_sandbox request json file",
		},
		cli.BoolFlag{
			Name:  "allow-internet-access",
			Usage: "set allowInternetAccess on the network config for the create_request",
		},
		cli.StringSliceFlag{
			Name:  "allow-out-cidr",
			Usage: "append an allowed egress CIDR to create_request.cube_network_config; repeat the flag to specify multiple CIDRs",
		},
		cli.StringSliceFlag{
			Name:  "deny-out-cidr",
			Usage: "append a denied egress CIDR to create_request.cube_network_config; repeat the flag to specify multiple CIDRs",
		},
		cli.BoolFlag{
			Name:  "detach, no-wait",
			Usage: "submit and exit immediately instead of watching the build to completion",
		},
		cli.DurationFlag{
			Name:  "interval",
			Value: defaultWatchInterval,
			Usage: "poll interval while watching the build",
		},
		cli.BoolFlag{
			Name:  "json",
			Usage: "print raw json response",
		},
	},
	Action: func(c *cli.Context) error {
		sandboxID := c.String("sandbox-id")
		filePath := c.String("file")
		if filePath == "" && c.NArg() > 0 {
			filePath = c.Args().First()
		}
		if sandboxID == "" || filePath == "" {
			return errors.New("sandbox-id and file are required")
		}

		reqBytes, err := getParams(filePath)
		if err != nil {
			return err
		}
		createReq := &types.CreateCubeSandboxReq{}
		if err = jsoniter.Unmarshal(reqBytes, createReq); err != nil {
			return err
		}
		if createReq.Request == nil {
			createReq.Request = &types.Request{}
		}
		requestID := uuid.New().String()
		createReq.RequestID = requestID
		if createReq.Annotations == nil {
			createReq.Annotations = map[string]string{}
		}
		createReq.CubeNetworkConfig = mergeCubeNetworkConfigFlags(c, createReq.CubeNetworkConfig)

		req := &templateCommitRequest{
			RequestID:     requestID,
			SandboxID:     sandboxID,
			CreateRequest: createReq,
		}

		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")
		host := serverList[rand.Int()%len(serverList)]
		url := fmt.Sprintf("http://%s/cube/sandbox/commit", net.JoinHostPort(host, port))
		body, err := jsoniter.Marshal(req)
		if err != nil {
			return err
		}

		rsp := &templateCommitResponse{}
		if err = doHttpReq(c, url, http.MethodPost, requestID, bytes.NewBuffer(body), rsp); err != nil {
			log.Printf("template commit request err. %s. RequestId: %s\n", err.Error(), requestID)
			return err
		}
		if rsp.Ret == nil {
			return errors.New("empty response")
		}
		if rsp.Ret.RetCode != 200 {
			log.Printf("template commit failed. %s. RequestId: %s\n", rsp.Ret.RetMsg, requestID)
			return errors.New(rsp.Ret.RetMsg)
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		log.Printf("template_id: %s\n", rsp.TemplateID)
		log.Printf("build_id: %s\n", rsp.BuildID)
		if detachRequested(c) || rsp.BuildID == "" {
			return nil
		}
		return runBuildWatch(c, rsp.BuildID)
	},
}
View Source
var TemplateCreateCommand = cli.Command{
	Name:  "create",
	Usage: "create template snapshots on healthy nodes",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "file, f",
			Usage: "template create request json file",
		},
		cli.StringFlag{
			Name:  "version",
			Value: "v2",
			Usage: "template version annotation",
		},
		cli.StringFlag{
			Name:  "snapshot-dir",
			Usage: "override snapshot dir passed to cubemaster",
		},
		cli.StringFlag{
			Name:  "instance-type",
			Usage: "override instance type in request body",
		},
		cli.StringSliceFlag{
			Name:  "node",
			Usage: "create template only on the specified node id or host ip; repeat to specify multiple nodes",
		},
		cli.BoolFlag{
			Name:  "allow-internet-access",
			Usage: "set allowInternetAccess on the network config for the template request",
		},
		cli.StringSliceFlag{
			Name:  "allow-out-cidr",
			Usage: "append an allowed egress CIDR to cube_network_config; repeat the flag to specify multiple CIDRs",
		},
		cli.StringSliceFlag{
			Name:  "deny-out-cidr",
			Usage: "append a denied egress CIDR to cube_network_config; repeat the flag to specify multiple CIDRs",
		},
		cli.BoolFlag{
			Name:  "json",
			Usage: "print raw json response",
		},
	},
	Action: func(c *cli.Context) error {
		filePath := c.String("file")
		if filePath == "" && c.NArg() > 0 {
			filePath = c.Args().First()
		}
		if filePath == "" {
			return errors.New("file is required")
		}

		reqBytes, err := getParams(filePath)
		if err != nil {
			return err
		}
		req := &types.CreateCubeSandboxReq{}
		if err = jsoniter.Unmarshal(reqBytes, req); err != nil {
			return err
		}
		if req.Request == nil {
			req.Request = &types.Request{}
		}
		req.RequestID = uuid.New().String()
		if req.Annotations == nil {
			req.Annotations = map[string]string{}
		}
		req.Annotations[constants.CubeAnnotationsAppSnapshotCreate] = "true"
		version := c.String("version")
		if version != "" {
			req.Annotations[constants.CubeAnnotationAppSnapshotVersion] = version
			req.Annotations[constants.CubeAnnotationAppSnapshotTemplateVersion] = version
		}
		if snapshotDir := c.String("snapshot-dir"); snapshotDir != "" {
			req.SnapshotDir = snapshotDir
		}
		if instanceType := c.String("instance-type"); instanceType != "" {
			req.InstanceType = instanceType
		}
		if scope := c.StringSlice("node"); len(scope) > 0 {
			req.DistributionScope = scope
		}
		req.CubeNetworkConfig = mergeCubeNetworkConfigFlags(c, req.CubeNetworkConfig)

		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			log.Printf("no server addr\n")
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")
		host := serverList[rand.Int()%len(serverList)]
		url := fmt.Sprintf("http://%s/cube/template", net.JoinHostPort(host, port))
		body, err := jsoniter.Marshal(req)
		if err != nil {
			return err
		}
		rsp := &templateResponse{}
		if err = doHttpReq(c, url, http.MethodPost, req.RequestID, bytes.NewBuffer(body), rsp); err != nil {
			log.Printf("template create request err. %s. RequestId: %s\n", err.Error(), req.RequestID)
			return err
		}
		if rsp.Ret == nil {
			return errors.New("empty response")
		}
		if rsp.Ret.RetCode != 200 {
			log.Printf("template create failed. %s. RequestId: %s\n", rsp.Ret.RetMsg, req.RequestID)
			return errors.New(rsp.Ret.RetMsg)
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		printTemplateSummary(rsp)
		return nil
	},
}
View Source
var TemplateCreateFromImageCommand = cli.Command{
	Name:  "create-from-image",
	Usage: "build ext4 rootfs from OCI image and create template asynchronously",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "image", Usage: "source OCI image reference"},
		cli.StringFlag{Name: "alias", Usage: "human-readable stable alias for the template (e.g. my-app); sandboxes can reference the template by this alias instead of the generated template ID; valid: [a-z0-9-], max 64 chars"},
		cli.StringFlag{Name: "writable-layer-size", Usage: "immutable writable layer size, e.g. 20Gi"},
		cli.StringSliceFlag{Name: "expose-port", Usage: "container port to expose for the template; repeat the flag to specify multiple ports"},
		cli.StringFlag{Name: "instance-type", Value: "cubebox", Usage: "instance type"},
		cli.StringFlag{Name: "network-type", Value: "tap", Usage: "network type"},
		cli.StringSliceFlag{Name: "node", Usage: "create template only on the specified node id or host ip; repeat to specify multiple nodes"},
		cli.BoolFlag{Name: "allow-internet-access", Usage: "set allowInternetAccess on the network config for the generated template request"},
		cli.StringSliceFlag{Name: "allow-out-cidr", Usage: "append an allowed egress CIDR to cube_network_config; repeat the flag to specify multiple CIDRs"},
		cli.StringSliceFlag{Name: "deny-out-cidr", Usage: "append a denied egress CIDR to cube_network_config; repeat the flag to specify multiple CIDRs"},
		cli.StringFlag{Name: "registry-username", Usage: "registry username"},
		cli.StringFlag{Name: "registry-password", Usage: "registry password"},
		cli.BoolFlag{Name: "enable-ivshmem", Usage: "boot the template build sandbox with ivshmem enabled"},

		cli.StringSliceFlag{Name: "cmd", Usage: "override container ENTRYPOINT (command); repeat for multiple elements, e.g. --cmd /bin/sh --cmd -c"},
		cli.StringSliceFlag{Name: "arg", Usage: "override container CMD (args); repeat for multiple elements"},
		cli.StringSliceFlag{Name: "env", Usage: "set environment variable, KEY=VALUE format; repeat for multiple envs"},
		cli.StringSliceFlag{Name: "dns", Usage: "set container DNS nameserver; repeat for multiple servers"},
		cli.IntFlag{Name: "probe", Usage: "enable HTTP GET probe on the specified port (e.g. --probe 9000); sets timeout_ms=30000, period_ms=500"},
		cli.StringFlag{Name: "probe-path", Value: "/health", Usage: "HTTP path for the readiness probe (default: /health); only effective when --probe is set"},
		cli.IntFlag{Name: "cpu", Value: 2000, Usage: "CPU millicores for the template container (default: 2000, i.e. 2 cores)"},
		cli.IntFlag{Name: "memory", Value: 2000, Usage: "Memory for the template container in MB (default: 2000 MB)"},
		cli.BoolTFlag{Name: "with-cube-ca", Usage: "bake the CubeEgress root CA at /etc/cube/ca/cube-root-ca.crt into the template rootfs so sandboxes trust CubeEgress's MITM. Pass --with-cube-ca=false to skip (default: true)"},
		cli.BoolFlag{Name: "detach, no-wait", Usage: "submit and exit immediately instead of watching the job to completion"},
		cli.DurationFlag{Name: "interval", Value: defaultWatchInterval, Usage: "poll interval while watching the job"},
		cli.BoolFlag{Name: "json", Usage: "print raw json response"},
	},
	Action: func(c *cli.Context) error {
		if c.String("image") == "" {
			return errors.New("image is required")
		}
		if c.String("writable-layer-size") == "" {
			return errors.New("writable-layer-size is required")
		}
		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")
		host := serverList[rand.Int()%len(serverList)]
		exposedPorts, err := parseExposePortFlags(c.StringSlice("expose-port"))
		if err != nil {
			return err
		}
		containerOverrides, err := parseContainerOverrides(c)
		if err != nil {
			return err
		}
		req := &types.CreateTemplateFromImageReq{
			Request:        &types.Request{RequestID: uuid.New().String()},
			SourceImageRef: c.String("image"),
			Alias:          c.String("alias"),

			WritableLayerSize:  c.String("writable-layer-size"),
			DistributionScope:  c.StringSlice("node"),
			ExposedPorts:       exposedPorts,
			InstanceType:       c.String("instance-type"),
			NetworkType:        c.String("network-type"),
			RegistryUsername:   c.String("registry-username"),
			RegistryPassword:   c.String("registry-password"),
			ContainerOverrides: containerOverrides,
		}

		withCubeCA := c.BoolT("with-cube-ca")
		req.WithCubeCA = &withCubeCA
		applyCreateFromImageIvshmemFlag(c, req)
		req.CubeNetworkConfig, err = mergeCreateFromImageCubeNetworkConfigFlags(c, req.CubeNetworkConfig)
		if err != nil {
			return err
		}
		body, err := jsoniter.Marshal(req)
		if err != nil {
			return err
		}
		url := fmt.Sprintf("http://%s/cube/template/from-image", net.JoinHostPort(host, port))
		rsp := &templateImageJobResponse{}
		if err := doHttpReq(c, url, http.MethodPost, req.RequestID, bytes.NewBuffer(body), rsp); err != nil {
			return err
		}
		if rsp.Ret == nil {
			return errors.New("empty response")
		}
		if rsp.Ret.RetCode != 200 {
			return errors.New(rsp.Ret.RetMsg)
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		if detachRequested(c) || rsp.Job == nil {
			printTemplateImageJob(rsp.Job)
			return nil
		}
		log.Printf("submitted template image job: job_id=%s template_id=%s\n", rsp.Job.JobID, rsp.Job.TemplateID)
		return runImageJobWatch(c, rsp.Job.JobID)
	},
}
View Source
var TemplateDeleteCommand = cli.Command{
	Name:      "delete",
	Usage:     "delete template metadata and node replicas",
	ArgsUsage: "<template-id>",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "template-id",
			Usage: "template id to delete",
		},
	},
	Action: func(c *cli.Context) error {
		templateID := resolveTemplateID(c)
		if templateID == "" {
			return errors.New("template-id is required")
		}

		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")
		requestID := uuid.New().String()
		host := serverList[rand.Int()%len(serverList)]
		url := fmt.Sprintf("http://%s/cube/template", net.JoinHostPort(host, port))

		req := &templateDeleteRequest{
			RequestID:  requestID,
			TemplateID: templateID,
		}
		body, err := jsoniter.Marshal(req)
		if err != nil {
			return err
		}

		rsp := &templateResponse{}
		if err := doHttpReq(c, url, http.MethodDelete, requestID, bytes.NewBuffer(body), rsp); err != nil {
			log.Printf("template delete request err. %s. RequestId: %s\n", err.Error(), requestID)
			return err
		}
		if rsp.Ret == nil {
			return errors.New("empty response")
		}
		if rsp.Ret.RetCode != 200 {
			log.Printf("template delete failed. %s. RequestId: %s\n", rsp.Ret.RetMsg, requestID)
			return errors.New(rsp.Ret.RetMsg)
		}
		log.Printf("template deleted: %s\n", templateID)
		return nil
	},
}
View Source
var TemplateInfoCommand = cli.Command{
	Name:      "info",
	Usage:     "show template metadata and node replicas",
	ArgsUsage: "<template-id>",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "template-id",
			Usage: "template id to query",
		},
		cli.BoolFlag{
			Name:  "include-request",
			Usage: "include the stored create request in the response",
		},
		cli.BoolFlag{
			Name:  "json",
			Usage: "print raw json response",
		},
	},
	Action: func(c *cli.Context) error {
		templateID := resolveTemplateID(c)
		if templateID == "" {
			return errors.New("template-id is required")
		}

		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			log.Printf("no server addr\n")
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")
		requestID := uuid.New().String()
		host := serverList[rand.Int()%len(serverList)]
		url := fmt.Sprintf("http://%s/cube/template?template_id=%s", net.JoinHostPort(host, port), templateID)
		if c.Bool("include-request") {
			url += "&include_request=true"
		}

		rsp := &templateResponse{}
		if err := doHttpReq(c, url, http.MethodGet, requestID, nil, rsp); err != nil {
			log.Printf("template info request err. %s. RequestId: %s\n", err.Error(), requestID)
			return err
		}
		if rsp.Ret == nil {
			return errors.New("empty response")
		}
		if rsp.Ret.RetCode != 200 {
			log.Printf("template info failed. %s. RequestId: %s\n", rsp.Ret.RetMsg, requestID)
			return errors.New(rsp.Ret.RetMsg)
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		printTemplateSummary(rsp)
		return nil
	},
}
View Source
var TemplateListCommand = cli.Command{
	Name:    "list",
	Aliases: []string{"ls"},
	Usage:   "list templates",
	Flags: []cli.Flag{
		cli.BoolFlag{
			Name:  "json",
			Usage: "print raw json response",
		},
		cli.StringFlag{
			Name:  "output,o",
			Usage: "output format, set to wide for more columns",
		},
	},
	Action: func(c *cli.Context) error {
		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			log.Printf("no server addr\n")
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")
		requestID := uuid.New().String()
		host := serverList[rand.Int()%len(serverList)]
		url := fmt.Sprintf("http://%s/cube/template", net.JoinHostPort(host, port))

		rsp := &templateListResponse{}
		if err := doHttpReq(c, url, http.MethodGet, requestID, nil, rsp); err != nil {
			log.Printf("template list request err. %s. RequestId: %s\n", err.Error(), requestID)
			return err
		}
		if rsp.Ret == nil {
			return errors.New("empty response")
		}
		if rsp.Ret.RetCode != 200 {
			log.Printf("template list failed. %s. RequestId: %s\n", rsp.Ret.RetMsg, requestID)
			return errors.New(rsp.Ret.RetMsg)
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		wideOutput := strings.EqualFold(strings.TrimSpace(c.String("output")), "wide")
		w := tabwriter.NewWriter(os.Stdout, 4, 8, 4, ' ', 0)
		tabHeader := "TEMPLATE_ID\tALIAS\tSTATUS\tJOB_ID\tCREATED_AT\tIMAGE_INFO"
		if wideOutput {
			tabHeader = "TEMPLATE_ID\tALIAS\tSTATUS\tJOB_ID\tLAST_ERROR\tCREATED_AT\tIMAGE_INFO"
		}
		fmt.Fprintln(w, tabHeader)
		for _, item := range rsp.Data {
			jobID := item.JobID
			if jobID == "" {
				jobID = "-"
			}
			alias := item.DisplayName
			if alias == "" {
				alias = "-"
			}
			if wideOutput {
				fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
					item.TemplateID, alias, item.Status, jobID, item.LastError, item.CreatedAt, item.ImageInfo)
				continue
			}
			fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
				item.TemplateID, alias, item.Status, jobID, item.CreatedAt, item.ImageInfo)
		}
		return w.Flush()
	},
}
View Source
var TemplateRedoCommand = cli.Command{
	Name:      "redo",
	Usage:     "redo a template on all, specific, or failed nodes",
	ArgsUsage: "<template-id>",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "template-id", Usage: "template id to redo"},
		cli.StringSliceFlag{Name: "node", Usage: "redo only the specified node id or host ip; repeat to specify multiple nodes"},
		cli.BoolFlag{Name: "failed-only", Usage: "redo only failed nodes"},
		cli.BoolFlag{Name: "wait", Usage: "deprecated: redo now waits by default; use --detach to opt out"},
		cli.BoolFlag{Name: "detach, no-wait", Usage: "submit and exit immediately instead of watching the redo job to completion"},
		cli.DurationFlag{Name: "interval", Value: defaultWatchInterval, Usage: "poll interval while watching the job"},
		cli.BoolFlag{Name: "json", Usage: "print raw json response"},
	},
	Action: func(c *cli.Context) error {
		templateID := resolveTemplateID(c)
		if templateID == "" {
			return errors.New("template-id is required")
		}
		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")
		host := serverList[rand.Int()%len(serverList)]
		req := &types.RedoTemplateFromImageReq{
			Request:           &types.Request{RequestID: uuid.New().String()},
			TemplateID:        templateID,
			DistributionScope: c.StringSlice("node"),
			FailedOnly:        c.Bool("failed-only"),
			Wait:              c.Bool("wait"),
		}
		body, err := jsoniter.Marshal(req)
		if err != nil {
			return err
		}
		url := fmt.Sprintf("http://%s/cube/template/redo", net.JoinHostPort(host, port))
		rsp := &templateImageJobResponse{}
		if err := doHttpReq(c, url, http.MethodPost, req.RequestID, bytes.NewBuffer(body), rsp); err != nil {
			return err
		}
		if rsp.Ret == nil {
			return errors.New("empty response")
		}
		if rsp.Ret.RetCode != 200 {
			return errors.New(rsp.Ret.RetMsg)
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		if detachRequested(c) || rsp.Job == nil {
			printTemplateImageJob(rsp.Job)
			return nil
		}
		log.Printf("submitted redo job: job_id=%s template_id=%s\n", rsp.Job.JobID, rsp.Job.TemplateID)
		return runImageJobWatch(c, rsp.Job.JobID)
	},
}
View Source
var TemplateRenderCommand = cli.Command{
	Name:  "render",
	Usage: "preview the effective sandbox request for a template",
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "file, f",
			Usage: "sandbox create request json file used as preview input",
		},
		cli.StringFlag{
			Name:  "template-id",
			Usage: "template id to preview; overrides the request file annotation",
		},
		cli.BoolFlag{
			Name:  "json",
			Usage: "print raw json response",
		},
	},
	Action: func(c *cli.Context) error {
		req := &types.CreateCubeSandboxReq{}
		filePath := c.String("file")
		if filePath == "" && c.NArg() > 0 {
			filePath = c.Args().First()
		}
		if filePath != "" {
			reqBytes, err := getParams(filePath)
			if err != nil {
				return err
			}
			if err = jsoniter.Unmarshal(reqBytes, req); err != nil {
				return err
			}
		}
		if req.Request == nil {
			req.Request = &types.Request{}
		}
		req.RequestID = uuid.New().String()
		if req.Annotations == nil {
			req.Annotations = map[string]string{}
		}
		if templateID := c.String("template-id"); templateID != "" {
			req.Annotations[constants.CubeAnnotationAppSnapshotTemplateID] = templateID
		}
		if req.Annotations[constants.CubeAnnotationAppSnapshotTemplateID] == "" {
			return errors.New("template-id is required, either in request file or flag")
		}
		if constants.GetAppSnapshotVersion(req.Annotations) == "" {
			constants.SetAppSnapshotVersion(req.Annotations, "v2")
		}

		serverList = getServerAddrs(c)
		if len(serverList) == 0 {
			log.Printf("no server addr\n")
			return errors.New("no server addr")
		}
		port = c.GlobalString("port")
		host := serverList[rand.Int()%len(serverList)]
		url := fmt.Sprintf("http://%s/cube/sandbox/preview", net.JoinHostPort(host, port))

		body, err := jsoniter.Marshal(req)
		if err != nil {
			return err
		}
		rsp := &sandboxPreviewResponse{}
		if err = doHttpReq(c, url, http.MethodPost, req.RequestID, bytes.NewBuffer(body), rsp); err != nil {
			log.Printf("template render request err. %s. RequestId: %s\n", err.Error(), req.RequestID)
			return err
		}
		if rsp.Ret == nil {
			return errors.New("empty response")
		}
		if rsp.Ret.RetCode != 200 {
			log.Printf("template render failed. %s. RequestId: %s\n", rsp.Ret.RetMsg, req.RequestID)
			return errors.New(rsp.Ret.RetMsg)
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		printSandboxPreviewSummary(rsp)
		return nil
	},
}
View Source
var TemplateStatusCommand = cli.Command{
	Name:  "status",
	Usage: "show create-from-image job status",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "job-id", Usage: "template image job id"},
		cli.BoolFlag{Name: "json", Usage: "print raw json response"},
	},
	Action: func(c *cli.Context) error {
		jobID := c.String("job-id")
		if jobID == "" {
			return errors.New("job-id is required")
		}
		rsp, err := fetchTemplateImageJob(c, jobID)
		if err != nil {
			return err
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		printTemplateImageJob(rsp.Job)
		return nil
	},
}
View Source
var TemplateWatchCommand = cli.Command{
	Name:  "watch",
	Usage: "watch create-from-image job progress until completion",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "job-id", Usage: "template image job id"},
		cli.DurationFlag{Name: "interval", Value: 2 * time.Second, Usage: "poll interval"},
		cli.BoolFlag{Name: "json", Usage: "print final raw json response"},
	},
	Action: func(c *cli.Context) error {
		jobID := c.String("job-id")
		if jobID == "" {
			return errors.New("job-id is required")
		}
		return runImageJobWatch(c, jobID)
	},
}
View Source
var VolumeCommand = cli.Command{
	Name:    "volume",
	Aliases: []string{"vol"},
	Usage:   "manage volumes (CubeMaster /cube/volume)",
	Subcommands: cli.Commands{
		VolumeListCommand,
		VolumeGetCommand,
		VolumeDeleteCommand,
	},
}
View Source
var VolumeDeleteCommand = cli.Command{
	Name:    "delete",
	Aliases: []string{"rm", "remove"},
	Usage:   "delete a volume by id",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "volume-id", Usage: "volume id to delete"},
		cli.BoolFlag{Name: "json", Usage: "print json response"},
	},
	Action: func(c *cli.Context) error {
		volumeID := resolveVolumeID(c)
		if volumeID == "" {
			return errors.New("volume id is required (positional arg or --volume-id)")
		}
		endpoint := "/cube/volume/" + url.PathEscape(volumeID)
		rsp := &volumeDeleteResponse{}
		if err := doVolumeReq(c, http.MethodDelete, endpoint, uuid.NewString(), nil, rsp); err != nil {
			return err
		}
		if err := ensureVolumeSuccessRet(rsp.Ret); err != nil {
			return err
		}
		if c.Bool("json") {
			commands.PrintAsJSON(rsp)
			return nil
		}
		fmt.Printf("deleted volume: %s\n", rsp.VolumeID)
		return nil
	},
}
View Source
var VolumeGetCommand = cli.Command{
	Name:    "get",
	Aliases: []string{"info", "describe"},
	Usage:   "get a volume by id (omits token and private_data)",
	Flags: []cli.Flag{
		cli.StringFlag{Name: "volume-id", Usage: "volume id to query"},
		cli.BoolFlag{Name: "json", Usage: "print json (without sensitive fields)"},
	},
	Action: func(c *cli.Context) error {
		volumeID := resolveVolumeID(c)
		if volumeID == "" {
			return errors.New("volume id is required (positional arg or --volume-id)")
		}
		endpoint := "/cube/volume/" + url.PathEscape(volumeID)
		rsp := &volumeGetResponse{}
		if err := doVolumeReq(c, http.MethodGet, endpoint, uuid.NewString(), nil, rsp); err != nil {
			return err
		}
		if err := ensureVolumeSuccessRet(rsp.Ret); err != nil {
			return err
		}
		view := volumeGetViewResponse{Ret: rsp.Ret}
		if rsp.Volume != nil {
			v := toVolumeView(rsp.Volume)
			view.Volume = &v
		}
		if c.Bool("json") {
			commands.PrintAsJSON(view)
			return nil
		}
		if view.Volume == nil {
			return errors.New("empty volume in response")
		}
		printVolumeInfo(view.Volume)
		return nil
	},
}
View Source
var VolumeListCommand = cli.Command{
	Name:    "list",
	Aliases: []string{"ls"},
	Usage:   "list volumes (omits token and private_data)",
	Flags: []cli.Flag{
		cli.BoolFlag{Name: "json", Usage: "print json (without sensitive fields)"},
	},
	Action: func(c *cli.Context) error {
		rsp := &volumeListResponse{}
		if err := doVolumeReq(c, http.MethodGet, "/cube/volume", uuid.NewString(), nil, rsp); err != nil {
			return err
		}
		if err := ensureVolumeSuccessRet(rsp.Ret); err != nil {
			return err
		}
		view := volumeListViewResponse{
			Ret:     rsp.Ret,
			Volumes: make([]volumeView, 0, len(rsp.Volumes)),
		}
		for i := range rsp.Volumes {
			view.Volumes = append(view.Volumes, toVolumeView(&rsp.Volumes[i]))
		}
		if c.Bool("json") {
			commands.PrintAsJSON(view)
			return nil
		}
		printVolumeList(view.Volumes)
		return nil
	},
}

Functions

func GoWithWaitGroup

func GoWithWaitGroup(wg *sync.WaitGroup, handler func(), panicHandlers ...func(panicError interface{}))

func HandleCrash

func HandleCrash(additionalHandlers ...func(panicError interface{}))

func WithRecover

func WithRecover(handler func(), panicHandlers ...func(panicError interface{}))

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL