From 1a93c16a2881ab9b63e16378469c7e466ce5f1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Wed, 23 Jun 2021 16:53:16 +0200 Subject: [PATCH] Move datacenter commands to new structure (#335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move datacenter commands to new structure Signed-off-by: Lukas Kämmerling --- cmd/hcloud/main.go | 19 +-- internal/cli/root.go | 5 +- internal/cmd/certificate/describe.go | 3 +- internal/cmd/datacenter/datacenter.go | 7 +- internal/cmd/datacenter/describe.go | 151 ++++++------------ internal/cmd/datacenter/list.go | 93 ++++------- internal/hcapi2/client.go | 14 +- internal/hcapi2/server_type.go | 58 ++++++- internal/hcapi2/zz_server_type_client_mock.go | 28 ++++ 9 files changed, 195 insertions(+), 183 deletions(-) diff --git a/cmd/hcloud/main.go b/cmd/hcloud/main.go index dfc7522e..2ab664b9 100644 --- a/cmd/hcloud/main.go +++ b/cmd/hcloud/main.go @@ -5,6 +5,7 @@ import ( "os" "github.com/hetznercloud/cli/internal/cli" + "github.com/hetznercloud/cli/internal/hcapi2" "github.com/hetznercloud/cli/internal/state" ) @@ -15,25 +16,25 @@ func init() { } func main() { - state := state.New() + cliState := state.New() - if state.ConfigPath != "" { - _, err := os.Stat(state.ConfigPath) + if cliState.ConfigPath != "" { + _, err := os.Stat(cliState.ConfigPath) switch { case err == nil: - if err := state.ReadConfig(); err != nil { - log.Fatalf("unable to read config file %q: %s\n", state.ConfigPath, err) + if err := cliState.ReadConfig(); err != nil { + log.Fatalf("unable to read config file %q: %s\n", cliState.ConfigPath, err) } case os.IsNotExist(err): break default: - log.Fatalf("unable to read config file %q: %s\n", state.ConfigPath, err) + log.Fatalf("unable to read config file %q: %s\n", cliState.ConfigPath, err) } } - state.ReadEnv() - - rootCommand := cli.NewRootCommand(state) + cliState.ReadEnv() + apiClient := hcapi2.NewClient(cliState.Client()) + rootCommand := cli.NewRootCommand(cliState, apiClient) if err := rootCommand.Execute(); err != nil { log.Fatalln(err) } diff --git a/internal/cli/root.go b/internal/cli/root.go index b5c727ba..0248ac7b 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -20,11 +20,12 @@ import ( "github.com/hetznercloud/cli/internal/cmd/sshkey" "github.com/hetznercloud/cli/internal/cmd/version" "github.com/hetznercloud/cli/internal/cmd/volume" + "github.com/hetznercloud/cli/internal/hcapi2" "github.com/hetznercloud/cli/internal/state" "github.com/spf13/cobra" ) -func NewRootCommand(state *state.State) *cobra.Command { +func NewRootCommand(state *state.State, client hcapi2.Client) *cobra.Command { cmd := &cobra.Command{ Use: "hcloud", Short: "Hetzner Cloud CLI", @@ -43,7 +44,7 @@ func NewRootCommand(state *state.State) *cobra.Command { completion.NewCommand(state), servertype.NewCommand(state), context.NewCommand(state), - datacenter.NewCommand(state), + datacenter.NewCommand(state, client), location.NewCommand(state), iso.NewCommand(state), volume.NewCommand(state), diff --git a/internal/cmd/certificate/describe.go b/internal/cmd/certificate/describe.go index 48ce79bd..75ae4769 100644 --- a/internal/cmd/certificate/describe.go +++ b/internal/cmd/certificate/describe.go @@ -54,6 +54,7 @@ var describeCmd = base.DescribeCmd{ if len(cert.UsedBy) == 0 { fmt.Println(" Certificate unused") } else { + lbClient := client.LoadBalancer() for _, ub := range cert.UsedBy { fmt.Printf(" - Type: %s", ub.Type) // Currently certificates can be only attached to load balancers. @@ -63,7 +64,7 @@ var describeCmd = base.DescribeCmd{ fmt.Printf(" - ID: %d", ub.ID) continue } - fmt.Printf(" - Name: %s", client.LoadBalancer().LoadBalancerName(ub.ID)) + fmt.Printf(" - Name: %s", lbClient.LoadBalancerName(ub.ID)) } } return nil diff --git a/internal/cmd/datacenter/datacenter.go b/internal/cmd/datacenter/datacenter.go index 8866f401..2eb1ae4e 100644 --- a/internal/cmd/datacenter/datacenter.go +++ b/internal/cmd/datacenter/datacenter.go @@ -1,11 +1,12 @@ package datacenter import ( + "github.com/hetznercloud/cli/internal/hcapi2" "github.com/hetznercloud/cli/internal/state" "github.com/spf13/cobra" ) -func NewCommand(cli *state.State) *cobra.Command { +func NewCommand(cli *state.State, client hcapi2.Client) *cobra.Command { cmd := &cobra.Command{ Use: "datacenter", Short: "Manage datacenters", @@ -14,8 +15,8 @@ func NewCommand(cli *state.State) *cobra.Command { DisableFlagsInUseLine: true, } cmd.AddCommand( - newListCommand(cli), - newDescribeCommand(cli), + listCmd.CobraCommand(cli.Context, client, cli), + describeCmd.CobraCommand(cli.Context, client, cli), ) return cmd } diff --git a/internal/cmd/datacenter/describe.go b/internal/cmd/datacenter/describe.go index d0aa3201..8019aec5 100644 --- a/internal/cmd/datacenter/describe.go +++ b/internal/cmd/datacenter/describe.go @@ -1,117 +1,60 @@ package datacenter import ( - "encoding/json" + "context" "fmt" - "github.com/hetznercloud/cli/internal/cmd/cmpl" - "github.com/hetznercloud/cli/internal/cmd/output" - "github.com/hetznercloud/cli/internal/cmd/util" - "github.com/hetznercloud/cli/internal/state" + "github.com/hetznercloud/cli/internal/cmd/base" + "github.com/hetznercloud/cli/internal/hcapi2" "github.com/hetznercloud/hcloud-go/hcloud" - "github.com/spf13/cobra" ) -func newDescribeCommand(cli *state.State) *cobra.Command { - cmd := &cobra.Command{ - Use: "describe [FLAGS] DATACENTER", - Short: "Describe a datacenter", - Args: cobra.ExactArgs(1), - ValidArgsFunction: cmpl.SuggestArgs(cmpl.SuggestCandidatesF(cli.DataCenterNames)), - TraverseChildren: true, - DisableFlagsInUseLine: true, - PreRunE: cli.EnsureToken, - RunE: cli.Wrap(runDescribe), - } - output.AddFlag(cmd, output.OptionJSON(), output.OptionFormat()) - return cmd -} - -func runDescribe(cli *state.State, cmd *cobra.Command, args []string) error { - outputFlags := output.FlagsForCommand(cmd) - - idOrName := args[0] - datacenter, resp, err := cli.Client().Datacenter.Get(cli.Context, idOrName) - if err != nil { - return err - } - if datacenter == nil { - return fmt.Errorf("datacenter not found: %s", idOrName) - } - - switch { - case outputFlags.IsSet("json"): - return describeJSON(resp) - case outputFlags.IsSet("format"): - return util.DescribeFormat(datacenter, outputFlags["format"][0]) - default: - return describeText(cli, datacenter) - } -} - -func describeText(cli *state.State, datacenter *hcloud.Datacenter) error { - fmt.Printf("ID:\t\t%d\n", datacenter.ID) - fmt.Printf("Name:\t\t%s\n", datacenter.Name) - fmt.Printf("Description:\t%s\n", datacenter.Description) - fmt.Printf("Location:\n") - fmt.Printf(" Name:\t\t%s\n", datacenter.Location.Name) - fmt.Printf(" Description:\t%s\n", datacenter.Location.Description) - fmt.Printf(" Country:\t%s\n", datacenter.Location.Country) - fmt.Printf(" City:\t\t%s\n", datacenter.Location.City) - fmt.Printf(" Latitude:\t%f\n", datacenter.Location.Latitude) - fmt.Printf(" Longitude:\t%f\n", datacenter.Location.Longitude) - fmt.Printf("Server Types:\n") - - serverTypesMap := map[int]*hcloud.ServerType{} - for _, t := range datacenter.ServerTypes.Available { - serverTypesMap[t.ID] = t - } - for _, t := range datacenter.ServerTypes.Supported { - serverTypesMap[t.ID] = t - } - for id := range serverTypesMap { - var err error - serverTypesMap[id], _, err = cli.Client().ServerType.GetByID(cli.Context, id) - if err != nil { - return fmt.Errorf("error fetching server type: %v", err) +var describeCmd = base.DescribeCmd{ + ResourceNameSingular: "datacenter", + ShortDescription: "Describe an datacenter", + JSONKeyGetByID: "datacenter", + JSONKeyGetByName: "datacenters", + NameSuggestions: func(c hcapi2.Client) func() []string { return c.Datacenter().Names }, + Fetch: func(ctx context.Context, client hcapi2.Client, idOrName string) (interface{}, *hcloud.Response, error) { + return client.Datacenter().Get(ctx, idOrName) + }, + PrintText: func(ctx context.Context, client hcapi2.Client, resource interface{}) error { + datacenter := resource.(*hcloud.Datacenter) + + fmt.Printf("ID:\t\t%d\n", datacenter.ID) + fmt.Printf("ID:\t\t%d\n", datacenter.ID) + fmt.Printf("Name:\t\t%s\n", datacenter.Name) + fmt.Printf("Description:\t%s\n", datacenter.Description) + fmt.Printf("Location:\n") + fmt.Printf(" Name:\t\t%s\n", datacenter.Location.Name) + fmt.Printf(" Description:\t%s\n", datacenter.Location.Description) + fmt.Printf(" Country:\t%s\n", datacenter.Location.Country) + fmt.Printf(" City:\t\t%s\n", datacenter.Location.City) + fmt.Printf(" Latitude:\t%f\n", datacenter.Location.Latitude) + fmt.Printf(" Longitude:\t%f\n", datacenter.Location.Longitude) + fmt.Printf("Server Types:\n") + + printServerTypes := func(list []*hcloud.ServerType) { + for _, t := range list { + fmt.Printf(" - ID:\t\t %d\n", t.ID) + fmt.Printf(" Name:\t %s\n", client.ServerType().ServerTypeName(t.ID)) + fmt.Printf(" Description: %s\n", client.ServerType().ServerTypeDescription(t.ID)) + } } - } - printServerTypes := func(list []*hcloud.ServerType, dataMap map[int]*hcloud.ServerType) { - for _, t := range list { - st := dataMap[t.ID] - fmt.Printf(" - ID:\t\t %d\n", st.ID) - fmt.Printf(" Name:\t %s\n", st.Name) - fmt.Printf(" Description: %s\n", st.Description) + fmt.Printf(" Available:\n") + if len(datacenter.ServerTypes.Available) > 0 { + printServerTypes(datacenter.ServerTypes.Available) + } else { + fmt.Printf(" No available server types\n") + } + fmt.Printf(" Supported:\n") + if len(datacenter.ServerTypes.Supported) > 0 { + printServerTypes(datacenter.ServerTypes.Supported) + } else { + fmt.Printf(" No supported server types\n") } - } - - fmt.Printf(" Available:\n") - if len(datacenter.ServerTypes.Available) > 0 { - printServerTypes(datacenter.ServerTypes.Available, serverTypesMap) - } else { - fmt.Printf(" No available server types\n") - } - fmt.Printf(" Supported:\n") - if len(datacenter.ServerTypes.Supported) > 0 { - printServerTypes(datacenter.ServerTypes.Supported, serverTypesMap) - } else { - fmt.Printf(" No supported server types\n") - } - - return nil -} -func describeJSON(resp *hcloud.Response) error { - var data map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - return err - } - if datacenter, ok := data["datacenter"]; ok { - return util.DescribeJSON(datacenter) - } - if datacenters, ok := data["datacenters"].([]interface{}); ok { - return util.DescribeJSON(datacenters[0]) - } - return util.DescribeJSON(data) + return nil + }, } diff --git a/internal/cmd/datacenter/list.go b/internal/cmd/datacenter/list.go index 6a17a07c..82177c8d 100644 --- a/internal/cmd/datacenter/list.go +++ b/internal/cmd/datacenter/list.go @@ -1,75 +1,46 @@ package datacenter import ( + "context" + + "github.com/hetznercloud/cli/internal/cmd/base" "github.com/hetznercloud/cli/internal/cmd/output" "github.com/hetznercloud/cli/internal/cmd/util" - "github.com/hetznercloud/cli/internal/state" + "github.com/hetznercloud/cli/internal/hcapi2" "github.com/hetznercloud/hcloud-go/hcloud" "github.com/hetznercloud/hcloud-go/hcloud/schema" - "github.com/spf13/cobra" ) -var listTableOutput *output.Table - -func init() { - listTableOutput = output.NewTable(). - AddAllowedFields(hcloud.Datacenter{}). - AddFieldFn("location", output.FieldFn(func(obj interface{}) string { - datacenter := obj.(*hcloud.Datacenter) - return datacenter.Location.Name - })) -} - -func newListCommand(cli *state.State) *cobra.Command { - cmd := &cobra.Command{ - Use: "list [FLAGS]", - Short: "List datacenters", - Long: util.ListLongDescription( - "Displays a list of datacenters.", - listTableOutput.Columns(), - ), - TraverseChildren: true, - DisableFlagsInUseLine: true, - PreRunE: cli.EnsureToken, - RunE: cli.Wrap(runList), - } - output.AddFlag(cmd, output.OptionNoHeader(), output.OptionColumns(listTableOutput.Columns()), output.OptionJSON()) - return cmd -} - -func runList(cli *state.State, cmd *cobra.Command, args []string) error { - outOpts := output.FlagsForCommand(cmd) +var listCmd = base.ListCmd{ + ResourceNamePlural: "datacenters", + DefaultColumns: []string{"id", "name", "description", "location"}, - datacenters, err := cli.Client().Datacenter.All(cli.Context) + Fetch: func(ctx context.Context, client hcapi2.Client, listOpts hcloud.ListOpts) ([]interface{}, error) { + datacenters, _, err := client.Datacenter().List(ctx, hcloud.DatacenterListOpts{ListOpts: listOpts}) - if outOpts.IsSet("json") { - var datacenterSchemas []schema.Datacenter - for _, datacenter := range datacenters { - datacenterSchemas = append(datacenterSchemas, util.DatacenterToSchema(*datacenter)) + var resources []interface{} + for _, n := range datacenters { + resources = append(resources, n) + } + return resources, err + }, + + OutputTable: func(_ hcapi2.Client) *output.Table { + return output.NewTable(). + AddAllowedFields(hcloud.Datacenter{}). + AddFieldFn("location", output.FieldFn(func(obj interface{}) string { + datacenter := obj.(*hcloud.Datacenter) + return datacenter.Location.Name + })) + }, + + JSONSchema: func(resources []interface{}) interface{} { + var certSchemas []schema.Datacenter + for _, resource := range resources { + cert := resource.(*hcloud.Datacenter) + certSchemas = append(certSchemas, util.DatacenterToSchema(*cert)) } - return util.DescribeJSON(datacenterSchemas) - } - - if err != nil { - return err - } - - cols := []string{"id", "name", "description", "location"} - if outOpts.IsSet("columns") { - cols = outOpts["columns"] - } - - tw := listTableOutput - if err = tw.ValidateColumns(cols); err != nil { - return err - } - if !outOpts.IsSet("noheader") { - tw.WriteHeader(cols) - } - for _, datacenter := range datacenters { - tw.Write(cols, datacenter) - } - tw.Flush() - return nil + return util.DescribeJSON(certSchemas) + }, } diff --git a/internal/hcapi2/client.go b/internal/hcapi2/client.go index 3cd3b6a4..f63ec824 100644 --- a/internal/hcapi2/client.go +++ b/internal/hcapi2/client.go @@ -1,6 +1,8 @@ package hcapi2 import ( + "sync" + "github.com/golang/mock/gomock" "github.com/hetznercloud/hcloud-go/hcloud" ) @@ -22,7 +24,10 @@ type Client interface { } type client struct { - client *hcloud.Client + client *hcloud.Client + serverTypeClient ServerTypeClient + + mu sync.Mutex } // NewClient creates a new CLI API client extending hcloud.Client. @@ -68,7 +73,12 @@ func (c *client) Server() ServerClient { } func (c *client) ServerType() ServerTypeClient { - return NewServerTypeClient(&c.client.ServerType) + c.mu.Lock() + if c.serverTypeClient == nil { + c.serverTypeClient = NewServerTypeClient(&c.client.ServerType) + } + defer c.mu.Unlock() + return c.serverTypeClient } func (c *client) SSHKey() SSHKeyClient { diff --git a/internal/hcapi2/server_type.go b/internal/hcapi2/server_type.go index f9adf09b..05eda586 100644 --- a/internal/hcapi2/server_type.go +++ b/internal/hcapi2/server_type.go @@ -1,10 +1,17 @@ package hcapi2 -import "context" +import ( + "context" + "github.com/hetznercloud/hcloud-go/hcloud" + "strconv" + "sync" +) type ServerTypeClient interface { ServerTypeClientBase Names() []string + ServerTypeName(id int) string + ServerTypeDescription(id int) string } func NewServerTypeClient(client ServerTypeClientBase) ServerTypeClient { @@ -15,6 +22,38 @@ func NewServerTypeClient(client ServerTypeClientBase) ServerTypeClient { type serverTypeClient struct { ServerTypeClientBase + + srvTypeByID map[int]*hcloud.ServerType + once sync.Once + err error +} + +// ServerTypeName obtains the name of the server type with id. If the name could not +// be fetched it returns the value id converted to a string. +func (c *serverTypeClient) ServerTypeName(id int) string { + if err := c.init(); err != nil { + return strconv.Itoa(id) + } + + serverType, ok := c.srvTypeByID[id] + if !ok || serverType.Name == "" { + return strconv.Itoa(id) + } + return serverType.Name +} + +// ServerTypeDescription obtains the description of the server type with id. If the name could not +// be fetched it returns the value id converted to a string. +func (c *serverTypeClient) ServerTypeDescription(id int) string { + if err := c.init(); err != nil { + return strconv.Itoa(id) + } + + serverType, ok := c.srvTypeByID[id] + if !ok || serverType.Description == "" { + return strconv.Itoa(id) + } + return serverType.Description } // Names returns a slice of all available server types. @@ -29,3 +68,20 @@ func (c *serverTypeClient) Names() []string { } return names } + +func (c *serverTypeClient) init() error { + c.once.Do(func() { + serverTypes, err := c.All(context.Background()) + if err != nil { + c.err = err + } + if c.err != nil || len(serverTypes) == 0 { + return + } + c.srvTypeByID = make(map[int]*hcloud.ServerType, len(serverTypes)) + for _, srv := range serverTypes { + c.srvTypeByID[srv.ID] = srv + } + }) + return c.err +} diff --git a/internal/hcapi2/zz_server_type_client_mock.go b/internal/hcapi2/zz_server_type_client_mock.go index 8165f22d..0dccda63 100644 --- a/internal/hcapi2/zz_server_type_client_mock.go +++ b/internal/hcapi2/zz_server_type_client_mock.go @@ -127,3 +127,31 @@ func (mr *MockServerTypeClientMockRecorder) Names() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Names", reflect.TypeOf((*MockServerTypeClient)(nil).Names)) } + +// ServerTypeDescription mocks base method. +func (m *MockServerTypeClient) ServerTypeDescription(arg0 int) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ServerTypeDescription", arg0) + ret0, _ := ret[0].(string) + return ret0 +} + +// ServerTypeDescription indicates an expected call of ServerTypeDescription. +func (mr *MockServerTypeClientMockRecorder) ServerTypeDescription(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerTypeDescription", reflect.TypeOf((*MockServerTypeClient)(nil).ServerTypeDescription), arg0) +} + +// ServerTypeName mocks base method. +func (m *MockServerTypeClient) ServerTypeName(arg0 int) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ServerTypeName", arg0) + ret0, _ := ret[0].(string) + return ret0 +} + +// ServerTypeName indicates an expected call of ServerTypeName. +func (mr *MockServerTypeClientMockRecorder) ServerTypeName(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerTypeName", reflect.TypeOf((*MockServerTypeClient)(nil).ServerTypeName), arg0) +}