Skip to content

Commit 2bd467d

Browse files
committed
feat(lb): add name resolver and argument completion
1 parent 1566da8 commit 2bd467d

6 files changed

Lines changed: 217 additions & 3 deletions

File tree

internal/commands/loadbalancer/show.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package loadbalancer
22

33
import (
44
"github.com/UpCloudLtd/upcloud-cli/internal/commands"
5+
"github.com/UpCloudLtd/upcloud-cli/internal/completion"
56
"github.com/UpCloudLtd/upcloud-cli/internal/output"
7+
"github.com/UpCloudLtd/upcloud-cli/internal/resolver"
68
"github.com/UpCloudLtd/upcloud-cli/internal/ui"
79
"github.com/UpCloudLtd/upcloud-go-api/v4/upcloud/request"
810
"github.com/jedib0t/go-pretty/v6/text"
@@ -22,8 +24,8 @@ func ShowCommand() commands.Command {
2224

2325
type showCommand struct {
2426
*commands.BaseCommand
25-
// resolver.CachingLoadBalancer
26-
// completion.LoadBalancer
27+
resolver.CachingLoadBalancer
28+
completion.LoadBalancer
2729
}
2830

2931
// Execute implements commands.MultipleArgumentCommand
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package completion
2+
3+
import (
4+
"github.com/UpCloudLtd/upcloud-cli/internal/service"
5+
"github.com/UpCloudLtd/upcloud-go-api/v4/upcloud/request"
6+
"github.com/spf13/cobra"
7+
)
8+
9+
// LoadBalancer implements argument completion for load balancers, by uuid or name.
10+
type LoadBalancer struct {
11+
}
12+
13+
// make sure LoadBalancer implements the interface
14+
var _ Provider = LoadBalancer{}
15+
16+
// CompleteArgument implements completion.Provider
17+
func (s LoadBalancer) CompleteArgument(svc service.AllServices, toComplete string) ([]string, cobra.ShellCompDirective) {
18+
loadbalancers, err := svc.GetLoadBalancers(&request.GetLoadBalancersRequest{})
19+
if err != nil {
20+
return None(toComplete)
21+
}
22+
var vals []string
23+
for _, lb := range loadbalancers {
24+
vals = append(vals, lb.UUID, lb.Name)
25+
}
26+
27+
return MatchStringPrefix(vals, toComplete, true), cobra.ShellCompDirectiveNoFileComp
28+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package completion_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/UpCloudLtd/upcloud-cli/internal/completion"
8+
smock "github.com/UpCloudLtd/upcloud-cli/internal/mock"
9+
"github.com/UpCloudLtd/upcloud-go-api/v4/upcloud"
10+
"github.com/spf13/cobra"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/mock"
13+
)
14+
15+
var mockLoadBalancers = []upcloud.LoadBalancer{
16+
{Name: "asd-1", UUID: "abcdef"},
17+
{Name: "asd-2", UUID: "abcghi"},
18+
{Name: "qwe-1", UUID: "jklmno"},
19+
}
20+
21+
func TestLoadBalancer_CompleteArgument(t *testing.T) {
22+
for _, test := range []struct {
23+
name string
24+
complete string
25+
expectedMatches []string
26+
expectedDirective cobra.ShellCompDirective
27+
}{
28+
{name: "Name/UUID - no match", complete: "pqr", expectedMatches: []string(nil), expectedDirective: cobra.ShellCompDirectiveNoFileComp},
29+
{name: "UUID - single match", complete: "jkl", expectedMatches: []string{"jklmno"}, expectedDirective: cobra.ShellCompDirectiveNoFileComp},
30+
{name: "UUID - multiple matches", complete: "abc", expectedMatches: []string{"abcdef", "abcghi"}, expectedDirective: cobra.ShellCompDirectiveNoFileComp},
31+
{name: "Name - one match", complete: "qwe", expectedMatches: []string{"qwe-1"}, expectedDirective: cobra.ShellCompDirectiveNoFileComp},
32+
{name: "Name - multiple matches", complete: "asd", expectedMatches: []string{"asd-1", "asd-2"}, expectedDirective: cobra.ShellCompDirectiveNoFileComp},
33+
} {
34+
t.Run(test.name, func(t *testing.T) {
35+
mService := new(smock.Service)
36+
mService.On("GetLoadBalancers", mock.Anything).Return(mockLoadBalancers, nil)
37+
completions, directive := completion.LoadBalancer{}.CompleteArgument(mService, test.complete)
38+
assert.Equal(t, test.expectedMatches, completions)
39+
assert.Equal(t, test.expectedDirective, directive)
40+
})
41+
}
42+
}
43+
44+
func TestLoadBalancer_CompleteArgumentServiceFail(t *testing.T) {
45+
mService := new(smock.Service)
46+
mService.On("GetLoadBalancers", mock.Anything).Return(nil, fmt.Errorf("MOCKFAIL"))
47+
completions, directive := completion.LoadBalancer{}.CompleteArgument(mService, "asd")
48+
assert.Nil(t, completions)
49+
assert.Equal(t, cobra.ShellCompDirectiveDefault, directive)
50+
}

internal/mock/mock.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,11 @@ func (m *Service) WaitForManagedDatabaseState(r *request.WaitForManagedDatabaseS
647647
}
648648

649649
func (m *Service) GetLoadBalancers(r *request.GetLoadBalancersRequest) ([]upcloud.LoadBalancer, error) {
650-
return nil, nil
650+
args := m.Called(r)
651+
if args[0] == nil {
652+
return nil, args.Error(1)
653+
}
654+
return args[0].([]upcloud.LoadBalancer), args.Error(1)
651655
}
652656

653657
func (m *Service) GetLoadBalancer(r *request.GetLoadBalancerRequest) (*upcloud.LoadBalancer, error) {

internal/resolver/loadbalancer.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package resolver
2+
3+
import (
4+
internal "github.com/UpCloudLtd/upcloud-cli/internal/service"
5+
"github.com/UpCloudLtd/upcloud-go-api/v4/upcloud/request"
6+
)
7+
8+
// CachingLoadBalancer implements resolver for servers, caching the results
9+
type CachingLoadBalancer struct{}
10+
11+
// make sure we implement the ResolutionProvider interface
12+
var _ ResolutionProvider = CachingLoadBalancer{}
13+
14+
// Get implements ResolutionProvider.Get
15+
func (s CachingLoadBalancer) Get(svc internal.AllServices) (Resolver, error) {
16+
loadbalancers, err := svc.GetLoadBalancers(&request.GetLoadBalancersRequest{})
17+
if err != nil {
18+
return nil, err
19+
}
20+
return func(arg string) (uuid string, err error) {
21+
rv := ""
22+
for _, lb := range loadbalancers {
23+
if lb.Name == arg || lb.UUID == arg {
24+
if rv != "" {
25+
return "", AmbiguousResolutionError(arg)
26+
}
27+
rv = lb.UUID
28+
}
29+
}
30+
if rv != "" {
31+
return rv, nil
32+
}
33+
return "", NotFoundError(arg)
34+
}, nil
35+
}
36+
37+
// PositionalArgumentHelp implements resolver.ResolutionProvider
38+
func (s CachingLoadBalancer) PositionalArgumentHelp() string {
39+
return "<UUID/Name...>" //nolint:goconst
40+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package resolver_test
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
smock "github.com/UpCloudLtd/upcloud-cli/internal/mock"
8+
"github.com/UpCloudLtd/upcloud-cli/internal/resolver"
9+
10+
"github.com/UpCloudLtd/upcloud-go-api/v4/upcloud"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/mock"
13+
)
14+
15+
var mockLoadBalancers = []upcloud.LoadBalancer{
16+
{Name: "asd", UUID: "abcdef"},
17+
{Name: "asd", UUID: "abcghi"},
18+
{Name: "qwe", UUID: "jklmno"},
19+
}
20+
21+
func TestLoadBalancerResolution(t *testing.T) {
22+
t.Run("UUID", func(t *testing.T) {
23+
mService := &smock.Service{}
24+
mService.On("GetLoadBalancers", mock.Anything).Return(mockLoadBalancers, nil)
25+
res := resolver.CachingLoadBalancer{}
26+
argResolver, err := res.Get(mService)
27+
assert.NoError(t, err)
28+
for _, db := range mockLoadBalancers {
29+
resolved, err := argResolver(db.UUID)
30+
assert.NoError(t, err)
31+
assert.Equal(t, db.UUID, resolved)
32+
}
33+
34+
// Make sure caching works, eg. we didn't call GetLoadBalancers more than once
35+
mService.AssertNumberOfCalls(t, "GetLoadBalancers", 1)
36+
})
37+
38+
t.Run("Name", func(t *testing.T) {
39+
mService := &smock.Service{}
40+
mService.On("GetLoadBalancers", mock.Anything).Return(mockLoadBalancers, nil)
41+
res := resolver.CachingLoadBalancer{}
42+
argResolver, err := res.Get(mService)
43+
assert.NoError(t, err)
44+
45+
db := mockLoadBalancers[2]
46+
resolved, err := argResolver(db.Name)
47+
assert.NoError(t, err)
48+
assert.Equal(t, db.UUID, resolved)
49+
// Make sure caching works, eg. we didn't call GetLoadBalancers more than once
50+
mService.AssertNumberOfCalls(t, "GetLoadBalancers", 1)
51+
})
52+
53+
t.Run("Failures", func(t *testing.T) {
54+
mService := &smock.Service{}
55+
mService.On("GetLoadBalancers", mock.Anything).Return(mockLoadBalancers, nil)
56+
57+
res := resolver.CachingLoadBalancer{}
58+
argResolver, err := res.Get(mService)
59+
assert.NoError(t, err)
60+
var resolved string
61+
62+
// Ambigous Name
63+
resolved, err = argResolver("asd")
64+
if !assert.Error(t, err) {
65+
t.FailNow()
66+
}
67+
assert.ErrorIs(t, err, resolver.AmbiguousResolutionError("asd"))
68+
assert.Equal(t, "", resolved)
69+
70+
// Not found
71+
resolved, err = argResolver("not-found")
72+
if !assert.Error(t, err) {
73+
t.FailNow()
74+
}
75+
assert.ErrorIs(t, err, resolver.NotFoundError("not-found"))
76+
assert.Equal(t, "", resolved)
77+
78+
// Make sure caching works, eg. we didn't call GetLoadBalancers more than once
79+
mService.AssertNumberOfCalls(t, "GetLoadBalancers", 1)
80+
})
81+
}
82+
83+
func TestFailingLoadBalancerResolution(t *testing.T) {
84+
mService := &smock.Service{}
85+
mService.On("GetLoadBalancers", mock.Anything).Return(nil, errors.New("MOCKERROR"))
86+
res := resolver.CachingLoadBalancer{}
87+
argResolver, err := res.Get(mService)
88+
assert.EqualError(t, err, "MOCKERROR")
89+
assert.Nil(t, argResolver)
90+
}

0 commit comments

Comments
 (0)