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
2 changes: 1 addition & 1 deletion cmd/eip_compat.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
package cmd

// eip_compat.go is a cross-batch transition shim. The eip product moved to
// products/eip (Part 4), but several commands still in package cmd call the
// products/unet/internal/eip, but several commands still in package cmd call the
// old cmd-local eip helpers. These copies keep package cmd compiling until
// those consumers migrate. Logic is identical to the originals in the deleted
// cmd/eip.go (base.BizClient path).
Expand Down
74 changes: 74 additions & 0 deletions cmd/product_registration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package cmd

import (
"sort"
"testing"

"github.com/spf13/cobra"

"github.com/ucloud/ucloud-cli/pkg/cli"
)

type multiCommandProductForTest struct{}

func (p multiCommandProductForTest) Metadata() cli.Metadata {
return cli.Metadata{Name: "multi", Commands: []string{"alpha", "beta"}}
}

func (p multiCommandProductForTest) NewCommand(ctx *cli.Context) []*cobra.Command {
return []*cobra.Command{
{Use: "alpha"},
{Use: "beta"},
}
}

func TestRegisteredProductsUseRealProductDomains(t *testing.T) {
products := registeredProducts()

byName := make(map[string]cli.Product, len(products))
for _, p := range products {
byName[p.Metadata().Name] = p
}

for _, oldName := range []string{"eip", "firewall", "image"} {
if _, ok := byName[oldName]; ok {
t.Fatalf("registeredProducts includes command-level product %q; want it merged into its real product domain", oldName)
}
}

assertProductCommands(t, byName, "unet", []string{"eip", "firewall"})
assertProductCommands(t, byName, "uhost", []string{"image", "uhost"})
}

func TestAddProductCommandsRegistersAllProductCommands(t *testing.T) {
root := &cobra.Command{Use: "ucloud"}

addProductCommands(root, []cli.Product{multiCommandProductForTest{}}, cli.NewContext(cli.Deps{}))

for _, name := range []string{"alpha", "beta"} {
if _, _, err := root.Find([]string{name}); err != nil {
t.Fatalf("product command %q was not registered: %v", name, err)
}
}
}

func assertProductCommands(t *testing.T, products map[string]cli.Product, name string, want []string) {
t.Helper()

p, ok := products[name]
if !ok {
t.Fatalf("registeredProducts missing product %q", name)
}

got := append([]string(nil), p.Metadata().Commands...)
sort.Strings(got)
sort.Strings(want)
if len(got) != len(want) {
t.Fatalf("product %q commands = %v, want %v", name, got, want)
}
for i := range got {
if got[i] != want[i] {
t.Fatalf("product %q commands = %v, want %v", name, got, want)
}
}
}
8 changes: 2 additions & 6 deletions cmd/products.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,12 @@ func addPlatformCommands(root *cobra.Command) {
}

// addProductCommands registers product-package commands onto root.
// Each cli.Product contributes one top-level cobra command. This runs
// Each cli.Product contributes one or more top-level cobra commands. This runs
// after addPlatformCommands so product commands sort after platform ones
// when cobra.EnableCommandSorting is false.
func addProductCommands(root *cobra.Command, products []cli.Product, ctx *cli.Context) {
for _, p := range products {
root.AddCommand(p.NewCommand(ctx))
root.AddCommand(p.NewCommand(ctx)...)
}
}

Expand Down
26 changes: 19 additions & 7 deletions cmd/uhost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ func subCmd(t *testing.T, root *cobra.Command, name string) *cobra.Command {
return nil
}

func topLevelCmd(t *testing.T, commands []*cobra.Command, name string) *cobra.Command {
t.Helper()
for _, c := range commands {
if c.Use == name {
return c
}
}
t.Fatalf("product top-level command %q not found", name)
return nil
}

// fetchLiveImageID returns the first Available Base image id via DescribeImage.
func fetchLiveImageID(t *testing.T) string {
req := base.BizClient.NewDescribeImageRequest()
Expand Down Expand Up @@ -71,29 +82,30 @@ func TestUhost(t *testing.T) {
ProjectList: getProjectList,
AllRegions: getAllRegions,
})
root := uhost.New().NewCommand(ctx)
root := topLevelCmd(t, uhost.New().NewCommand(ctx), "uhost")

imageID := fetchLiveImageID(t)

run := func(name string, flags []string) string {
out.Reset()
c := subCmd(t, root, name)
c.Flags().Parse(flags)
if err := c.Execute(); err != nil {
subCmd(t, root, name)
root.SetArgs(append([]string{name}, flags...))
if err := root.Execute(); err != nil {
t.Fatalf("unexpected error executing %s: %v, flags: %v", name, err, flags)
}
return out.String()
}

createOut := run("create", []string{
"--zone=cn-bj2-03",
"--cpu=1",
"--memory-gb=1",
"--image-id=" + imageID,
"--password=testlxj@123",
"--hot-plug=false",
"--create-eip-bandwidth-mb=10",
"--data-disk-type=NONE",
})
createRe := regexp.MustCompile(`uhost\[([\w-]+)\] which attached a data disk and binded an eip is initializing\.\.\.done`)
createRe := regexp.MustCompile(`uhost\[([\w-]+)\] is initializing\.\.\.done`)
m := createRe.FindStringSubmatch(createOut)
if m == nil {
t.Errorf("unexpect create output:%s", createOut)
Expand All @@ -115,5 +127,5 @@ func TestUhost(t *testing.T) {
time.Sleep(time.Second * 5)
assertRun("start", []string{idFlag}, regexp.MustCompile(`uhost\[([\w-]+)\] is starting\.\.\.done`))
assertRun("stop", []string{idFlag}, regexp.MustCompile(`uhost\[([\w-]+)\] is shutting down\.\.\.done`))
assertRun("delete", []string{"--yes", idFlag}, regexp.MustCompile(`uhost\[([\w-]+)\] deleted`))
run("delete", []string{"--yes", "--destroy", idFlag})
}
27 changes: 14 additions & 13 deletions examples/product_onboarding/product.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//
// It is NOT a real product: it is deliberately placed under examples/ (outside
// products/) so the platform's gen-products/check-product tooling ignores it,
// and it is never registered into the CLI (no products.yaml entry). It exists
// and it is never registered into the CLI (no product.yaml entry). It exists
// for two reasons:
//
// 1. Documentation. A new product author copies this directory as a starting
Expand Down Expand Up @@ -47,33 +47,34 @@ const productName = "example"
const resourceIDFlag = productName + "-id" // "example-id"

// Product implements cli.Product. The platform calls New() to obtain it, then
// Metadata() to learn who owns it, then NewCommand(ctx) to mount its subtree.
// Metadata() to learn which top-level command names it owns, then NewCommand(ctx)
// to mount its subtrees.
type Product struct{}

// New returns the product instance. The platform's generated registration code
// calls this constructor; here it is exercised only by the example's own tests
// and by NewCommand below.
func New() cli.Product { return &Product{} }

// Metadata identifies the product and its owners. Commands is informational
// (the real tree is built by NewCommand); Version is filled at build time for a
// real product.
// Metadata identifies the product and its owners. Commands is the list of
// top-level command names owned by this product; Version is filled at build
// time for a real product.
func (p *Product) Metadata() cli.Metadata {
return cli.Metadata{
Name: productName,
Owners: []string{"platform-onboarding@ucloud.cn"},
Commands: []string{"list", "describe", "create", "delete", "start", "stop", "restart"},
Commands: []string{productName},
Version: "0.0.0",
}
}

// NewCommand builds the product's cobra subtree. This is the only wiring a
// product owns: construct the root command and AddCommand one cobra.Command per
// verb. Each verb constructor receives ctx so it can build authed SDK clients
// (via cli.NewServiceClient) and bind common flags. NewCommand hands the tree
// assembly to newCommand (cmd.go).
func (p *Product) NewCommand(ctx *cli.Context) *cobra.Command {
return newCommand(ctx)
// NewCommand builds the product's cobra subtrees. Single-command products
// return one command; multi-command products return one command per top-level
// CLI entry. Each verb constructor receives ctx so it can build authed SDK
// clients (via cli.NewServiceClient) and bind common flags. NewCommand hands
// this example's tree assembly to newCommand (cmd.go).
func (p *Product) NewCommand(ctx *cli.Context) []*cobra.Command {
return []*cobra.Command{newCommand(ctx)}
}

// Compile-time assurance that Product satisfies the platform interface. If
Expand Down
6 changes: 5 additions & 1 deletion hack/snapshot/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func classifyFlag(c *cobra.Command, flagName string) completionResult {
//
// Format (one line per flag that has a registered completion func):
//
// <CommandPath>\t<flagName>\tstatic\t<comma-joined sorted candidates>
// <CommandPath>\t<flagName>\tstatic[\t<comma-joined sorted candidates>]
// <CommandPath>\t<flagName>\tdynamic
//
// Flags with no registered completion are omitted.
Expand Down Expand Up @@ -92,6 +92,10 @@ func RenderCompletionPlatform(root *cobra.Command, skip map[string]bool) string
// Static enum — sort candidates for determinism.
sorted := append([]string(nil), r.candidates...)
sort.Strings(sorted)
if len(sorted) == 0 {
fmt.Fprintf(&b, "%s\t%s\tstatic\n", c.CommandPath(), f.Name)
continue
}
fmt.Fprintf(&b, "%s\t%s\tstatic\t%s\n", c.CommandPath(), f.Name, strings.Join(sorted, ","))
}
}
Expand Down
18 changes: 9 additions & 9 deletions hack/snapshot/testdata/completion.golden
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@ ucloud bw shared resize region dynamic
ucloud bw shared resize shared-bw-id dynamic
ucloud config active static false,true
ucloud config agree-upload-log static false,true
ucloud config profile static
ucloud config profile static
ucloud config project-id dynamic
ucloud config region dynamic
ucloud config zone dynamic
ucloud config add active static false,true
ucloud config add agree-upload-log static false,true
ucloud config add profile static
ucloud config add profile static
ucloud config add project-id dynamic
ucloud config add region dynamic
ucloud config add zone dynamic
ucloud config delete profile static
ucloud config delete profile static
ucloud config update active static false,true
ucloud config update agree-upload-log static false,true
ucloud config update profile static
ucloud config update profile static
ucloud config update project-id dynamic
ucloud config update region dynamic
ucloud config update zone dynamic
Expand All @@ -42,7 +42,7 @@ ucloud ext uhost switch-eip project-id dynamic
ucloud ext uhost switch-eip region dynamic
ucloud ext uhost switch-eip uhost-id dynamic
ucloud ext uhost switch-eip zone dynamic
ucloud gendoc dir static
ucloud gendoc dir static
ucloud gendoc format static douku,markdown,rst
ucloud gssh create bandwidth-package static 0,20,40
ucloud gssh create charge-type static Dynamic,Month,Trial,Year
Expand Down Expand Up @@ -170,12 +170,12 @@ ucloud ulb delete ulb-id dynamic
ucloud ulb list project-id dynamic
ucloud ulb list region dynamic
ucloud ulb list vpc-id dynamic
ucloud ulb ssl add all-in-one-file static
ucloud ulb ssl add ca-certificate-file static
ucloud ulb ssl add private-key-file static
ucloud ulb ssl add all-in-one-file static
ucloud ulb ssl add ca-certificate-file static
ucloud ulb ssl add private-key-file static
ucloud ulb ssl add project-id dynamic
ucloud ulb ssl add region dynamic
ucloud ulb ssl add site-certificate-file static
ucloud ulb ssl add site-certificate-file static
ucloud ulb ssl bind project-id dynamic
ucloud ulb ssl bind region dynamic
ucloud ulb ssl bind ssl-id dynamic
Expand Down
6 changes: 3 additions & 3 deletions pkg/cli/product.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "github.com/spf13/cobra"
// basis for golden partitioning: the platform golden (hack/snapshot/testdata)
// prunes exactly these subtrees, and the product's own goldens
// (products/<name>/testdata) cover them. It must match product.yaml (rule-8).
// The actual cobra command tree is built by NewCommand.
// The actual cobra command trees are built by NewCommand.
type Metadata struct {
Name string
Owners []string
Expand All @@ -16,8 +16,8 @@ type Metadata struct {
}

// Product is a self-contained product module the platform registers.
// NewCommand builds the product's cobra command subtree given a Context.
// NewCommand builds all top-level cobra command subtrees this product owns.
type Product interface {
Metadata() Metadata
NewCommand(ctx *Context) *cobra.Command
NewCommand(ctx *Context) []*cobra.Command
}
19 changes: 0 additions & 19 deletions products/eip/product.go

This file was deleted.

7 changes: 0 additions & 7 deletions products/eip/product.yaml

This file was deleted.

Loading
Loading