Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ ucloud-sandbox-cli sandbox create desktop
ucloud-sandbox-cli sandbox create base
```

创建沙箱时可以按 Volume 名称挂载一个或多个持久化 Volume:

```bash
ucloud-sandbox-cli sandbox create base --mount <volume-name>:/data
ucloud-sandbox-cli sandbox create base \
--mount <volume-name-1>:/data \
--mount <volume-name-2>:/cache
```

> 创建成功后,CLI 会自动连接终端,您可以像操作本地 Shell 一样执行命令。按`Ctrl+D`或输入`exit`退出连接(沙箱继续运行)。

### 连接现有沙箱
Expand Down Expand Up @@ -166,6 +175,27 @@ ucloud-sandbox-cli sandbox metrics <sandbox-id>
ucloud-sandbox-cli sandbox metrics <sandbox-id> -w
```

## Volume 管理

创建持久化 Volume:

```bash
ucloud-sandbox-cli vol create <name>
```

列出 Volume:

```bash
ucloud-sandbox-cli vol list
ucloud-sandbox-cli vol list --format json
```

删除一个或多个 Volume:

```bash
ucloud-sandbox-cli vol delete <volume-id...>
```

## 模板构建管理

### 初始化模板项目
Expand Down
29 changes: 27 additions & 2 deletions cmd/sandbox/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@ package sandbox
import (
"context"
"fmt"
"strings"

"github.com/spf13/cobra"
sdk "github.com/ucloud/ucloud-sandbox-sdk-go"
"github.com/ucloud/ucloud-sandbox-cli/internal/config"
sdk "github.com/ucloud/ucloud-sandbox-sdk-go"
)

func newCreateCmd() *cobra.Command {
var timeout int
var detach bool
var mountSpecs []string

cmd := &cobra.Command{
Use: "create [template]",
Aliases: []string{"cr"},
Short: "Create a new sandbox",
Args: cobra.MaximumNArgs(1),
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
template := "base"
if len(args) > 0 {
Expand All @@ -38,6 +40,13 @@ func newCreateCmd() *cobra.Command {
if timeout > 0 {
opts = append(opts, sdk.WithTimeout(timeout))
}
if len(mountSpecs) > 0 {
mounts, err := parseVolumeMounts(mountSpecs)
if err != nil {
return err
}
opts = append(opts, sdk.WithVolumeMounts(mounts))
}

sbx, err := client.CreateSandbox(ctx, opts...)
if err != nil {
Expand All @@ -55,5 +64,21 @@ func newCreateCmd() *cobra.Command {

cmd.Flags().IntVar(&timeout, "timeout", 0, "Sandbox timeout in seconds")
cmd.Flags().BoolVar(&detach, "detach", false, "Do not connect to the sandbox after creation")
cmd.Flags().StringArrayVar(&mountSpecs, "mount", nil, "Mount volume as <volume-name>:<path> (repeatable)")
return cmd
}

func parseVolumeMounts(values []string) ([]sdk.VolumeMount, error) {
mounts := make([]sdk.VolumeMount, 0, len(values))
for _, value := range values {
volumeName, mountPath, ok := strings.Cut(value, ":")
if !ok || volumeName == "" || mountPath == "" {
return nil, fmt.Errorf("invalid mount %q: expected <volume-name>:<absolute-path>", value)
}
if !strings.HasPrefix(mountPath, "/") {
return nil, fmt.Errorf("invalid mount %q: mount path must be absolute", value)
}
mounts = append(mounts, sdk.VolumeMount{Name: volumeName, Path: mountPath})
}
return mounts, nil
}
37 changes: 37 additions & 0 deletions cmd/volume/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package volume

import (
"fmt"

"github.com/spf13/cobra"
"github.com/ucloud/ucloud-sandbox-cli/internal/config"
)

func newCreateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "create <name>",
Aliases: []string{"cr"},
Short: "Create a volume",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return err
}
client, err := config.NewClient(cfg)
if err != nil {
return err
}

volume, err := client.CreateVolume(cmd.Context(), args[0])
if err != nil {
return fmt.Errorf("failed to create volume: %w", err)
}

fmt.Fprintf(cmd.OutOrStdout(), "Volume created: %s\n", volume.ID)
return nil
},
}

return cmd
}
43 changes: 43 additions & 0 deletions cmd/volume/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package volume

import (
"fmt"

"github.com/spf13/cobra"
"github.com/ucloud/ucloud-sandbox-cli/internal/config"
)

func newDeleteCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "delete <volume-id...>",
Aliases: []string{"dl"},
Short: "Delete one or more volumes",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return err
}
client, err := config.NewClient(cfg)
if err != nil {
return err
}

for _, id := range args {
deleted, err := client.DeleteVolume(cmd.Context(), id)
if err != nil {
return fmt.Errorf("failed to delete volume %s: %w", id, err)
}
if !deleted {
fmt.Fprintf(cmd.OutOrStdout(), "Volume not found: %s\n", id)
continue
}
fmt.Fprintf(cmd.OutOrStdout(), "Deleted volume: %s\n", id)
}

return nil
},
}

return cmd
}
73 changes: 73 additions & 0 deletions cmd/volume/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package volume

import (
"encoding/json"
"fmt"

"github.com/spf13/cobra"
"github.com/ucloud/ucloud-sandbox-cli/internal/config"
"github.com/ucloud/ucloud-sandbox-cli/internal/table"
sdk "github.com/ucloud/ucloud-sandbox-sdk-go"
)

// listedVolume is a display-friendly view of VolumeInfo for table rendering.
type listedVolume struct {
VolumeID string `table_field:"Volume ID"`
Name string `table_field:"Name"`
}

func toListedVolume(v sdk.VolumeInfo) listedVolume {
return listedVolume{
VolumeID: v.VolumeID,
Name: v.Name,
}
}

func newListCmd() *cobra.Command {
var format string

cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List volumes",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return err
}
client, err := config.NewClient(cfg)
if err != nil {
return err
}

volumes, err := client.ListVolumes(cmd.Context())
if err != nil {
return err
}

if format == "json" {
return json.NewEncoder(cmd.OutOrStdout()).Encode(volumes)
}

if len(volumes) == 0 {
fmt.Fprintln(cmd.OutOrStdout(), "No volumes found.")
return nil
}

rows := make([]listedVolume, len(volumes))
for i, volume := range volumes {
rows[i] = toListedVolume(volume)
}

out, err := table.Render(rows, 1, 0, int64(len(rows)))
if err != nil {
return err
}
fmt.Fprint(cmd.OutOrStdout(), out)
return nil
},
}

cmd.Flags().StringVarP(&format, "format", "f", "pretty", "Output format (pretty, json)")
return cmd
}
16 changes: 16 additions & 0 deletions cmd/volume/volume.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package volume

import "github.com/spf13/cobra"

// NewVolumeCmd returns the root volume command group.
func NewVolumeCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "volume",
Aliases: []string{"vol"},
Short: "Manage volumes",
}
cmd.AddCommand(newCreateCmd())
cmd.AddCommand(newDeleteCmd())
cmd.AddCommand(newListCmd())
return cmd
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/manifoldco/promptui v0.9.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260724023741-905601aa3144
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260811030544-8dd599016a25
golang.org/x/term v0.44.0
)

Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260626103758-60b4523c8358 h1:q9
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260626103758-60b4523c8358/go.mod h1:097WABFNud50hH2KImyZ8WOJeb0lM2A4+gLvQ1kllsc=
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260724023741-905601aa3144 h1:PHVW7cDYIAfl1vv/6qKbvzs52HvhlSW5j1h4DuJO2uc=
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260724023741-905601aa3144/go.mod h1:097WABFNud50hH2KImyZ8WOJeb0lM2A4+gLvQ1kllsc=
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260803102739-bc8a7a31fc6a h1:NAPrfCttc0J30dMXBYd71oqpB0+Z1hJi7o/JbqZc1u0=
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260803102739-bc8a7a31fc6a/go.mod h1:097WABFNud50hH2KImyZ8WOJeb0lM2A4+gLvQ1kllsc=
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260804080039-35258adf309e h1:HUxJzyqzi7HfasPmtGB6894+7a60i7kJCzzxFzMr6fk=
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260804080039-35258adf309e/go.mod h1:097WABFNud50hH2KImyZ8WOJeb0lM2A4+gLvQ1kllsc=
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260811030544-8dd599016a25 h1:l9KU2bJKvLGZkh8NDKq0fnzbSF7B2P7Eo8pN6b20ccw=
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260811030544-8dd599016a25/go.mod h1:097WABFNud50hH2KImyZ8WOJeb0lM2A4+gLvQ1kllsc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
sandboxcmd "github.com/ucloud/ucloud-sandbox-cli/cmd/sandbox"
snapshotcmd "github.com/ucloud/ucloud-sandbox-cli/cmd/snapshot"
templatecmd "github.com/ucloud/ucloud-sandbox-cli/cmd/template"
volumecmd "github.com/ucloud/ucloud-sandbox-cli/cmd/volume"
)

var (
Expand All @@ -36,6 +37,7 @@ func newCommand() *cobra.Command {
c.AddCommand(fscmd.NewFsCmd())
c.AddCommand(snapshotcmd.NewSnapshotCmd())
c.AddCommand(templatecmd.NewTemplateCmd())
c.AddCommand(volumecmd.NewVolumeCmd())

versionCmd := &cobra.Command{
Use: "version",
Expand Down
27 changes: 12 additions & 15 deletions skills/astraflow-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,23 +85,20 @@ export ASTRAFLOW_PROJECT_ID="<project-id>"
- 浮点数不能用科学计数法表示。
- 数组类型参数(例如 `ModelNames.N`)按其展开后的实际键名参与排序和拼接,比如 `ModelNames.0`、`ModelNames.1`。

示例(来自官方文档,用于校验实现是否正确):
安全示例(只使用占位符和环境变量,不在文档中写入任何可用密钥):

- `PublicKey`: `ucloudsomeone@example.com1296235120854146120`
- `PrivateKey`: `46f09bb9fab4f12dfc160dae12273d5332b5debe`
- 请求参数:`Action=DescribeUHostInstance`、`Region=cn-bj2`、`Limit=10`
- 拼接后的待签名字符串:
- `PublicKey`:从 `ASTRAFLOW_PUBLIC_KEY` 读取。
- `PrivateKey`:只从 `ASTRAFLOW_PRIVATE_KEY` 读取,不写入命令字面量、日志或仓库文件。
- 请求参数:`Action=DescribeUHostInstance`、`Region=cn-bj2`、`Limit=10`。

```
ActionDescribeUHostInstanceLimit10PublicKeyucloudsomeone@example.com1296235120854146120Regioncn-bj246f09bb9fab4f12dfc160dae12273d5332b5debe
```

- 对上面字符串做SHA1,得到 `Signature`:`cba5cf5ec4d4233d206b1b54951e3787350a642f`

用shell快速验证的写法(仅用于本地校验签名算法实现,实际调用时按参数升序拼接对应接口的真实参数):
用 shell 在本地计算签名,并将结果保存到环境变量(执行前确认未开启 `set -x`):

```bash
printf '%s' 'ActionDescribeUHostInstanceLimit10PublicKeyucloudsomeone@example.com1296235120854146120Regioncn-bj246f09bb9fab4f12dfc160dae12273d5332b5debe' | sha1sum
ASTRAFLOW_SIGNATURE="$(
printf '%s' "ActionDescribeUHostInstanceLimit10PublicKey${ASTRAFLOW_PUBLIC_KEY}Regioncn-bj2${ASTRAFLOW_PRIVATE_KEY}" |
sha1sum |
awk '{print $1}'
)"
```

计算出Signature后,把它作为一个普通参数加进最终请求里,和其余参数一起发送。
Expand All @@ -115,9 +112,9 @@ curl -X POST \
-d '{
"Action" : "DescribeUHostInstance",
"Limit" : 10,
"PublicKey" : "ucloudsomeone@example.com1296235120854146120",
"PublicKey" : "<value-from-ASTRAFLOW_PUBLIC_KEY>",
"Region" : "cn-bj2",
"Signature" : "cba5cf5ec4d4233d206b1b54951e3787350a642f"
"Signature" : "<value-from-ASTRAFLOW_SIGNATURE>"
}'
```

Expand Down
Loading