Skip to content
Merged
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ cd my-custom-env
ucloud-sandbox-cli tpl build my-custom-env
```

可使用 `--level` 控制构建日志的最低等级(`debug`、`info`、`warn` 或 `error`),默认为 `info`:

```bash
ucloud-sandbox-cli tpl build my-custom-env --level debug
```

### 发布模板

默认情况下,模板只能由您当前的项目访问,如果您需要其他人也能使用模板,需要公开:
Expand Down
126 changes: 98 additions & 28 deletions cmd/template/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,33 @@ package template
import (
"context"
"fmt"
"os"
"path/filepath"

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

const (
fallbackDockerfileName = "Dockerfile"
defaultDockerfileName = "template.dockerfile"
)

type buildFlags struct {
path string
dockerfile string
startCmd string
readyCmd string
cpuCount int
memoryMB int
cpuSet bool
memorySet bool
noCache bool
publish bool
tags []string
logLevel string
}

func newBuildCmd() *cobra.Command {
Expand All @@ -34,15 +44,20 @@ func buildCommand(use string, aliases []string, short string) *cobra.Command {
var flags buildFlags

cmd := &cobra.Command{
Use: use + " <template-name>",
Use: use + " [template-name]",
Aliases: aliases,
Short: short,
Args: cobra.ExactArgs(1),
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runBuild(args[0], &flags)
flags.cpuSet = cmd.Flags().Changed("cpu-count")
flags.memorySet = cmd.Flags().Changed("memory-mb")
name := ""
if len(args) == 1 {
name = args[0]
}
return runBuild(name, &flags)
},
}

cmd.Flags().StringVarP(&flags.path, "path", "p", ".", "Build context path")
cmd.Flags().StringVarP(&flags.dockerfile, "dockerfile", "d", "", "Dockerfile path")
cmd.Flags().StringVar(&flags.startCmd, "cmd", "", "Start command")
Expand All @@ -52,27 +67,45 @@ func buildCommand(use string, aliases []string, short string) *cobra.Command {
cmd.Flags().BoolVar(&flags.noCache, "no-cache", false, "Skip build cache")
cmd.Flags().BoolVar(&flags.publish, "publish", false, "Publish after build")
cmd.Flags().StringSliceVarP(&flags.tags, "tag", "t", nil, "Build tags")

cmd.Flags().StringVar(&flags.logLevel, "level", "info", "Minimum build log level (debug, info, warn, error)")
return cmd
}

func runBuild(name string, flags *buildFlags) error {
// 1. Validate name
contextPath, localCfg := resolveBuildContext(name, flags.path)
if name == "" && localCfg != nil {
name = localCfg.TemplateName
}
if name == "" {
return fmt.Errorf("template name is required; provide it as an argument or in ucloud-template.json")
}
if err := template.ValidateName(name); err != nil {
return err
}

// 2. Validate memory (must be even)
if localCfg != nil {
if !flags.cpuSet && localCfg.CPUCount > 0 {
flags.cpuCount = localCfg.CPUCount
}
if !flags.memorySet && localCfg.MemoryMB > 0 {
flags.memoryMB = localCfg.MemoryMB
}
if flags.dockerfile == "" && localCfg.Dockerfile != "" {
flags.dockerfile = localCfg.Dockerfile
}
}
if flags.cpuCount <= 0 {
return fmt.Errorf("CPU count must be greater than zero, got %d", flags.cpuCount)
}
if flags.memoryMB <= 0 {
return fmt.Errorf("memory must be greater than zero, got %d", flags.memoryMB)
}
if flags.memoryMB%2 != 0 {
return fmt.Errorf("memory must be an even number, got %d", flags.memoryMB)
}

// 3. Validate start/ready cmd
if flags.startCmd == "" && flags.readyCmd != "" {
return fmt.Errorf("both --cmd and --ready-cmd must be provided together")
}

// 4. Load config and create client
cfg, err := config.Load()
if err != nil {
return err
Expand All @@ -81,44 +114,81 @@ func runBuild(name string, flags *buildFlags) error {
if err != nil {
return err
}

// 5. Build template
ctx := context.Background()

// Create builder (simplified: FromBaseImage + RunCmd)
builder := sdk.NewTemplate(sdk.WithFileContextPath(flags.path)).FromBaseImage()

dockerfilePath, err := resolveDockerfilePath(contextPath, flags.dockerfile)
if err != nil {
return err
}
builder, err := sdk.NewTemplate(sdk.WithFileContextPath(contextPath)).FromDockerfile(dockerfilePath)
if err != nil {
return err
}
if flags.startCmd != "" {
builder = builder.SetStartCmd(flags.startCmd, sdk.ReadyCmd{Cmd: flags.readyCmd})
builder.SetStartCmd(flags.startCmd, sdk.ReadyCmd{Cmd: flags.readyCmd})
}

// Build options
opts := []sdk.BuildOption{
sdk.WithBuildCPUCount(flags.cpuCount),
sdk.WithBuildMemoryMB(flags.memoryMB),
sdk.WithBuildSkipCache(flags.noCache),
sdk.WithOnBuildLogs(sdk.DefaultBuildLogger()),
sdk.WithOnBuildLogs(sdk.DefaultBuildLoggerWithLevel(flags.logLevel)),
}
if len(flags.tags) > 0 {
opts = append(opts, sdk.WithBuildTags(flags.tags))
}
if flags.publish {
opts = append(opts, sdk.WithPublishTemplate())
}

fmt.Println("\nBuilding sandbox template...")
fmt.Println()

info, err := client.BuildTemplate(ctx, builder, name, opts...)
info, err := client.BuildTemplate(context.Background(), builder, name, opts...)
if err != nil {
return fmt.Errorf("build failed: %w", err)
}

// 6. Print success
fmt.Printf("\n✅ Building sandbox template finished.\n")
fmt.Printf("Template ID: %s\n", info.TemplateID)
fmt.Printf("Build ID: %s\n", info.BuildID)
fmt.Printf("\nYou can now use the template to create sandboxes.\n")

return nil
}

// resolveBuildContext prefers the template's named directory, then --path itself.
func resolveBuildContext(name, path string) (string, *LocalConfig) {
candidate := filepath.Join(path, name)
if cfg, err := loadConfig(candidate); err == nil {
return candidate, cfg
}
if cfg, err := loadConfig(path); err == nil {
return path, cfg
}
return path, nil
}

// resolveDockerfilePath resolves the Dockerfile path from context or explicit input.
func resolveDockerfilePath(contextPath, explicit string) (string, error) {
if explicit != "" {
path := explicit
if !filepath.IsAbs(path) {
path = filepath.Join(contextPath, path)
}
if _, err := os.Stat(path); err != nil {
return "", fmt.Errorf("dockerfile %q: %w", path, err)
}
return path, nil
}
if cfg, err := loadConfig(contextPath); err == nil && cfg.Dockerfile != "" {
path := cfg.Dockerfile
if !filepath.IsAbs(path) {
path = filepath.Join(contextPath, path)
}
if _, err := os.Stat(path); err != nil {
return "", fmt.Errorf("dockerfile %q: %w", path, err)
}
return path, nil
}
for _, filename := range []string{defaultDockerfileName, fallbackDockerfileName} {
path := filepath.Join(contextPath, filename)
if _, err := os.Stat(path); err == nil {
return path, nil
}
}
return "", fmt.Errorf("no Dockerfile found in %q", contextPath)
}
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,25 @@ require (
github.com/schollz/progressbar/v3 v3.19.1
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260821080716-2ddf6dabf0ad
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260901065209-638d8b157dc0
golang.org/x/mod v0.40.0
golang.org/x/term v0.44.0
)

require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/moby/buildkit v0.20.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/sys v0.46.0 // indirect
google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
47 changes: 45 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,35 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/guptarohit/asciigraph v0.10.0 h1:LmbFXSHZOhaQxjJYexdRk7TzoC5sJ7vDTEjP1YUbKgY=
github.com/guptarohit/asciigraph v0.10.0/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/moby/buildkit v0.20.2 h1:qIeR47eQ1tzI1rwz0on3Xx2enRw/1CKjFhoONVcTlMA=
github.com/moby/buildkit v0.20.2/go.mod h1:DhaF82FjwOElTftl0JUAJpH/SUIUx4UvcFncLeOtlDI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
Expand All @@ -34,16 +48,45 @@ github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260821080716-2ddf6dabf0ad h1:neWZlKqEofByXO5bNwgGQoj6A76sf/bcqjhsh1YAqF0=
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260821080716-2ddf6dabf0ad/go.mod h1:097WABFNud50hH2KImyZ8WOJeb0lM2A4+gLvQ1kllsc=
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260901065209-638d8b157dc0 h1:qopX0t0fSPmUEo1bYbbO8Fp7BM+LFACMetgxAA5xEEM=
github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260901065209-638d8b157dc0/go.mod h1:YrCrvJEqOBr33ToGyKcFB+ZcTY0EkueE4H0uyqNdsb8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
Loading