Skip to content

Commit

Permalink
refactor: split up label command into separate Fetch/GetLabels/GetIDO…
Browse files Browse the repository at this point in the history
…rName (#967)

This change is intended for an upcoming resource that has labels but
doesn't have numeric IDs.
  • Loading branch information
phm07 authored Jan 31, 2025
1 parent 0ccd41c commit d846f49
Show file tree
Hide file tree
Showing 23 changed files with 290 additions and 182 deletions.
38 changes: 22 additions & 16 deletions internal/cmd/base/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,20 @@ import (
)

// LabelCmds allows defining commands for adding labels to resources.
type LabelCmds struct {
type LabelCmds[T any] struct {
ResourceNameSingular string
ShortDescriptionAdd string
ShortDescriptionRemove string
NameSuggestions func(client hcapi2.Client) func() []string
LabelKeySuggestions func(client hcapi2.Client) func(idOrName string) []string
FetchLabels func(s state.State, idOrName string) (map[string]string, int64, error)
SetLabels func(s state.State, id int64, labels map[string]string) error
Fetch func(s state.State, idOrName string) (T, error)
SetLabels func(s state.State, resource T, labels map[string]string) error
GetLabels func(resource T) map[string]string
GetIDOrName func(resource T) string
}

// AddCobraCommand creates a command that can be registered with cobra.
func (lc *LabelCmds) AddCobraCommand(s state.State) *cobra.Command {
func (lc *LabelCmds[T]) AddCobraCommand(s state.State) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("add-label [--overwrite] <%s> <label>...", util.ToKebabCase(lc.ResourceNameSingular)),
Short: lc.ShortDescriptionAdd,
Expand All @@ -43,15 +45,17 @@ func (lc *LabelCmds) AddCobraCommand(s state.State) *cobra.Command {
}

// RunAdd executes an add label command
func (lc *LabelCmds) RunAdd(s state.State, cmd *cobra.Command, args []string) error {
func (lc *LabelCmds[T]) RunAdd(s state.State, cmd *cobra.Command, args []string) error {
overwrite, _ := cmd.Flags().GetBool("overwrite")
idOrName := args[0]

labels, id, err := lc.FetchLabels(s, idOrName)
resource, err := lc.Fetch(s, idOrName)
if err != nil {
return err
}

labels, idOrName := lc.GetLabels(resource), lc.GetIDOrName(resource)

if labels == nil {
labels = map[string]string{}
}
Expand All @@ -62,17 +66,17 @@ func (lc *LabelCmds) RunAdd(s state.State, cmd *cobra.Command, args []string) er
keys = append(keys, key)

if _, ok := labels[key]; ok && !overwrite {
return fmt.Errorf("label %s on %s %d already exists", key, lc.ResourceNameSingular, id)
return fmt.Errorf("label %s on %s %s already exists", key, lc.ResourceNameSingular, idOrName)
}

labels[key] = val
}

if err := lc.SetLabels(s, id, labels); err != nil {
if err := lc.SetLabels(s, resource, labels); err != nil {
return err
}

cmd.Printf("Label(s) %s added to %s %d\n", strings.Join(keys, ", "), lc.ResourceNameSingular, id)
cmd.Printf("Label(s) %s added to %s %s\n", strings.Join(keys, ", "), lc.ResourceNameSingular, idOrName)
return nil
}

Expand All @@ -87,7 +91,7 @@ func validateAddLabel(_ *cobra.Command, args []string) error {
}

// RemoveCobraCommand creates a command that can be registered with cobra.
func (lc *LabelCmds) RemoveCobraCommand(s state.State) *cobra.Command {
func (lc *LabelCmds[T]) RemoveCobraCommand(s state.State) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("remove-label <%s> (--all | <label>...)", util.ToKebabCase(lc.ResourceNameSingular)),
Short: lc.ShortDescriptionRemove,
Expand All @@ -113,34 +117,36 @@ func (lc *LabelCmds) RemoveCobraCommand(s state.State) *cobra.Command {
}

// RunRemove executes a remove label command
func (lc *LabelCmds) RunRemove(s state.State, cmd *cobra.Command, args []string) error {
func (lc *LabelCmds[T]) RunRemove(s state.State, cmd *cobra.Command, args []string) error {
all, _ := cmd.Flags().GetBool("all")
idOrName := args[0]

labels, id, err := lc.FetchLabels(s, idOrName)
resource, err := lc.Fetch(s, idOrName)
if err != nil {
return err
}

labels, idOrName := lc.GetLabels(resource), lc.GetIDOrName(resource)

if all {
labels = make(map[string]string)
} else {
for _, key := range args[1:] {
if _, ok := labels[key]; !ok {
return fmt.Errorf("label %s on %s %d does not exist", key, lc.ResourceNameSingular, id)
return fmt.Errorf("label %s on %s %s does not exist", key, lc.ResourceNameSingular, idOrName)
}
delete(labels, key)
}
}

if err := lc.SetLabels(s, id, labels); err != nil {
if err := lc.SetLabels(s, resource, labels); err != nil {
return err
}

if all {
cmd.Printf("All labels removed from %s %d\n", lc.ResourceNameSingular, id)
cmd.Printf("All labels removed from %s %s\n", lc.ResourceNameSingular, idOrName)
} else {
cmd.Printf("Label(s) %s removed from %s %d\n", strings.Join(args[1:], ", "), lc.ResourceNameSingular, id)
cmd.Printf("Label(s) %s removed from %s %s\n", strings.Join(args[1:], ", "), lc.ResourceNameSingular, idOrName)
}

return nil
Expand Down
21 changes: 14 additions & 7 deletions internal/cmd/certificate/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,41 @@ package certificate

import (
"fmt"
"strconv"

"github.com/hetznercloud/cli/internal/cmd/base"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
)

var LabelCmds = base.LabelCmds{
var LabelCmds = base.LabelCmds[*hcloud.Certificate]{
ResourceNameSingular: "certificate",
ShortDescriptionAdd: "Add a label to an certificate",
ShortDescriptionRemove: "Remove a label from an certificate",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.Certificate().Names },
LabelKeySuggestions: func(c hcapi2.Client) func(idOrName string) []string { return c.Certificate().LabelKeys },
FetchLabels: func(s state.State, idOrName string) (map[string]string, int64, error) {
Fetch: func(s state.State, idOrName string) (*hcloud.Certificate, error) {
certificate, _, err := s.Client().Certificate().Get(s, idOrName)
if err != nil {
return nil, 0, err
return nil, err
}
if certificate == nil {
return nil, 0, fmt.Errorf("certificate not found: %s", idOrName)
return nil, fmt.Errorf("certificate not found: %s", idOrName)
}
return certificate.Labels, certificate.ID, nil
return certificate, nil
},
SetLabels: func(s state.State, id int64, labels map[string]string) error {
SetLabels: func(s state.State, cert *hcloud.Certificate, labels map[string]string) error {
opts := hcloud.CertificateUpdateOpts{
Labels: labels,
}
_, _, err := s.Client().Certificate().Update(s, &hcloud.Certificate{ID: id}, opts)
_, _, err := s.Client().Certificate().Update(s, cert, opts)
return err
},
GetLabels: func(cert *hcloud.Certificate) map[string]string {
return cert.Labels
},
GetIDOrName: func(cert *hcloud.Certificate) string {
return strconv.FormatInt(cert.ID, 10)
},
}
16 changes: 9 additions & 7 deletions internal/cmd/certificate/labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,18 @@ func TestLabelRemove(t *testing.T) {
cmd := certificate.LabelCmds.RemoveCobraCommand(fx.State())
fx.ExpectEnsureToken()

cert := &hcloud.Certificate{
ID: 123,
Labels: map[string]string{
"key": "value",
},
}

fx.Client.CertificateClient.EXPECT().
Get(gomock.Any(), "123").
Return(&hcloud.Certificate{
ID: 123,
Labels: map[string]string{
"key": "value",
},
}, nil, nil)
Return(cert, nil, nil)
fx.Client.CertificateClient.EXPECT().
Update(gomock.Any(), &hcloud.Certificate{ID: 123}, hcloud.CertificateUpdateOpts{
Update(gomock.Any(), cert, hcloud.CertificateUpdateOpts{
Labels: make(map[string]string),
})

Expand Down
21 changes: 14 additions & 7 deletions internal/cmd/firewall/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,41 @@ package firewall

import (
"fmt"
"strconv"

"github.com/hetznercloud/cli/internal/cmd/base"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
)

var LabelCmds = base.LabelCmds{
var LabelCmds = base.LabelCmds[*hcloud.Firewall]{
ResourceNameSingular: "firewall",
ShortDescriptionAdd: "Add a label to an firewall",
ShortDescriptionRemove: "Remove a label from an firewall",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.Firewall().Names },
LabelKeySuggestions: func(c hcapi2.Client) func(idOrName string) []string { return c.Firewall().LabelKeys },
FetchLabels: func(s state.State, idOrName string) (map[string]string, int64, error) {
Fetch: func(s state.State, idOrName string) (*hcloud.Firewall, error) {
firewall, _, err := s.Client().Firewall().Get(s, idOrName)
if err != nil {
return nil, 0, err
return nil, err
}
if firewall == nil {
return nil, 0, fmt.Errorf("firewall not found: %s", idOrName)
return nil, fmt.Errorf("firewall not found: %s", idOrName)
}
return firewall.Labels, firewall.ID, nil
return firewall, nil
},
SetLabels: func(s state.State, id int64, labels map[string]string) error {
SetLabels: func(s state.State, firewall *hcloud.Firewall, labels map[string]string) error {
opts := hcloud.FirewallUpdateOpts{
Labels: labels,
}
_, _, err := s.Client().Firewall().Update(s, &hcloud.Firewall{ID: id}, opts)
_, _, err := s.Client().Firewall().Update(s, firewall, opts)
return err
},
GetLabels: func(firewall *hcloud.Firewall) map[string]string {
return firewall.Labels
},
GetIDOrName: func(firewall *hcloud.Firewall) string {
return strconv.FormatInt(firewall.ID, 10)
},
}
16 changes: 9 additions & 7 deletions internal/cmd/firewall/labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,18 @@ func TestLabelRemove(t *testing.T) {
cmd := firewall.LabelCmds.RemoveCobraCommand(fx.State())
fx.ExpectEnsureToken()

fw := &hcloud.Firewall{
ID: 123,
Labels: map[string]string{
"key": "value",
},
}

fx.Client.FirewallClient.EXPECT().
Get(gomock.Any(), "123").
Return(&hcloud.Firewall{
ID: 123,
Labels: map[string]string{
"key": "value",
},
}, nil, nil)
Return(fw, nil, nil)
fx.Client.FirewallClient.EXPECT().
Update(gomock.Any(), &hcloud.Firewall{ID: 123}, hcloud.FirewallUpdateOpts{
Update(gomock.Any(), fw, hcloud.FirewallUpdateOpts{
Labels: make(map[string]string),
})

Expand Down
21 changes: 14 additions & 7 deletions internal/cmd/floatingip/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,41 @@ package floatingip

import (
"fmt"
"strconv"

"github.com/hetznercloud/cli/internal/cmd/base"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
)

var LabelCmds = base.LabelCmds{
var LabelCmds = base.LabelCmds[*hcloud.FloatingIP]{
ResourceNameSingular: "Floating IP",
ShortDescriptionAdd: "Add a label to an Floating IP",
ShortDescriptionRemove: "Remove a label from an Floating IP",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.FloatingIP().Names },
LabelKeySuggestions: func(c hcapi2.Client) func(idOrName string) []string { return c.FloatingIP().LabelKeys },
FetchLabels: func(s state.State, idOrName string) (map[string]string, int64, error) {
Fetch: func(s state.State, idOrName string) (*hcloud.FloatingIP, error) {
floatingIP, _, err := s.Client().FloatingIP().Get(s, idOrName)
if err != nil {
return nil, 0, err
return nil, err
}
if floatingIP == nil {
return nil, 0, fmt.Errorf("floating IP not found: %s", idOrName)
return nil, fmt.Errorf("floating IP not found: %s", idOrName)
}
return floatingIP.Labels, floatingIP.ID, nil
return floatingIP, nil
},
SetLabels: func(s state.State, id int64, labels map[string]string) error {
SetLabels: func(s state.State, floatingIP *hcloud.FloatingIP, labels map[string]string) error {
opts := hcloud.FloatingIPUpdateOpts{
Labels: labels,
}
_, _, err := s.Client().FloatingIP().Update(s, &hcloud.FloatingIP{ID: id}, opts)
_, _, err := s.Client().FloatingIP().Update(s, floatingIP, opts)
return err
},
GetLabels: func(floatingIP *hcloud.FloatingIP) map[string]string {
return floatingIP.Labels
},
GetIDOrName: func(floatingIP *hcloud.FloatingIP) string {
return strconv.FormatInt(floatingIP.ID, 10)
},
}
16 changes: 9 additions & 7 deletions internal/cmd/floatingip/labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,18 @@ func TestLabelRemove(t *testing.T) {
cmd := floatingip.LabelCmds.RemoveCobraCommand(fx.State())
fx.ExpectEnsureToken()

floatingIP := &hcloud.FloatingIP{
ID: 123,
Labels: map[string]string{
"key": "value",
},
}

fx.Client.FloatingIPClient.EXPECT().
Get(gomock.Any(), "123").
Return(&hcloud.FloatingIP{
ID: 123,
Labels: map[string]string{
"key": "value",
},
}, nil, nil)
Return(floatingIP, nil, nil)
fx.Client.FloatingIPClient.EXPECT().
Update(gomock.Any(), &hcloud.FloatingIP{ID: 123}, hcloud.FloatingIPUpdateOpts{
Update(gomock.Any(), floatingIP, hcloud.FloatingIPUpdateOpts{
Labels: make(map[string]string),
})

Expand Down
22 changes: 14 additions & 8 deletions internal/cmd/image/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,37 @@ import (
"github.com/hetznercloud/hcloud-go/v2/hcloud"
)

var LabelCmds = base.LabelCmds{
var LabelCmds = base.LabelCmds[*hcloud.Image]{
ResourceNameSingular: "image",
ShortDescriptionAdd: "Add a label to an image",
ShortDescriptionRemove: "Remove a label from an image",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.Image().Names },
LabelKeySuggestions: func(c hcapi2.Client) func(idOrName string) []string { return c.Image().LabelKeys },
FetchLabels: func(s state.State, idOrName string) (map[string]string, int64, error) {
Fetch: func(s state.State, idOrName string) (*hcloud.Image, error) {
id, err := strconv.ParseInt(idOrName, 10, 64)
if err != nil {
return nil, 0, fmt.Errorf("invalid snapshot or backup ID %q", idOrName)
return nil, fmt.Errorf("invalid snapshot or backup ID %q", idOrName)
}
image, _, err := s.Client().Image().GetByID(s, id)
if err != nil {
return nil, 0, err
return nil, err
}
if image == nil {
return nil, 0, fmt.Errorf("image not found: %s", idOrName)
return nil, fmt.Errorf("image not found: %s", idOrName)
}
return image.Labels, image.ID, nil
return image, nil
},
SetLabels: func(s state.State, id int64, labels map[string]string) error {
SetLabels: func(s state.State, image *hcloud.Image, labels map[string]string) error {
opts := hcloud.ImageUpdateOpts{
Labels: labels,
}
_, _, err := s.Client().Image().Update(s, &hcloud.Image{ID: id}, opts)
_, _, err := s.Client().Image().Update(s, image, opts)
return err
},
GetLabels: func(image *hcloud.Image) map[string]string {
return image.Labels
},
GetIDOrName: func(image *hcloud.Image) string {
return strconv.FormatInt(image.ID, 10)
},
}
Loading

0 comments on commit d846f49

Please sign in to comment.