Skip to content

Commit 187565e

Browse files
committed
feat(partner): add Partner API support
1 parent 74756ea commit 187565e

File tree

12 files changed

+382
-12
lines changed

12 files changed

+382
-12
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Partner API support
13+
14+
### Changed
15+
16+
- Go version bump to 1.22
17+
1018
## [3.12.0] - 2024-11-18
1119

1220
### Changed

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
module github.com/UpCloudLtd/upcloud-cli/v3
22

3-
go 1.21
3+
go 1.22
44

55
require (
66
github.com/UpCloudLtd/progress v1.0.2
7-
github.com/UpCloudLtd/upcloud-go-api/v8 v8.7.0
7+
github.com/UpCloudLtd/upcloud-go-api/v8 v8.14.0
88
github.com/adrg/xdg v0.3.2
99
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
1010
github.com/gemalto/flume v0.12.0
@@ -14,7 +14,7 @@ require (
1414
github.com/spf13/cobra v1.6.1
1515
github.com/spf13/pflag v1.0.5
1616
github.com/spf13/viper v1.7.1
17-
github.com/stretchr/testify v1.8.4
17+
github.com/stretchr/testify v1.9.0
1818
golang.org/x/crypto v0.21.0
1919
golang.org/x/term v0.18.0
2020
gopkg.in/yaml.v3 v3.0.1
@@ -46,7 +46,7 @@ require (
4646
github.com/spf13/afero v1.2.2 // indirect
4747
github.com/spf13/cast v1.3.0 // indirect
4848
github.com/spf13/jwalterweatherman v1.0.0 // indirect
49-
github.com/stretchr/objx v0.5.0 // indirect
49+
github.com/stretchr/objx v0.5.2 // indirect
5050
github.com/subosito/gotenv v1.2.0 // indirect
5151
go.uber.org/atomic v1.6.0 // indirect
5252
go.uber.org/multierr v1.5.0 // indirect

go.sum

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
1717
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
1818
github.com/UpCloudLtd/progress v1.0.2 h1:CTr1bBuFuXop9TEhR1PakbUMPTlUVL7Bgae9JgqXwPg=
1919
github.com/UpCloudLtd/progress v1.0.2/go.mod h1:iGxOnb9HvHW0yrLGUjHr0lxHhn7TehgWwh7a8NqK6iQ=
20-
github.com/UpCloudLtd/upcloud-go-api/v8 v8.7.0 h1:BAWDRZQRRFkF7KOnX2D7/r0FUtwo/6vgxtEp4kwYTMw=
21-
github.com/UpCloudLtd/upcloud-go-api/v8 v8.7.0/go.mod h1:/BL9bYxio0GCdotzBvZjkpm1fSDtD0+0z6PtNMew9HU=
20+
github.com/UpCloudLtd/upcloud-go-api/v8 v8.14.0 h1:bJozr/MtrSl4P3ynq4Nkr8kGPQfPAGpGJ7/S/iVI1cc=
21+
github.com/UpCloudLtd/upcloud-go-api/v8 v8.14.0/go.mod h1:bFnrOkfsDDmsb94nnBV5eSQjjsfDnwAzLnCt9+b4t/4=
2222
github.com/adrg/xdg v0.3.2 h1:GUSGQ5pHdev83AYhDSS1A/CX+0JIsxbiWtow2DSA+RU=
2323
github.com/adrg/xdg v0.3.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
2424
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -256,17 +256,16 @@ github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
256256
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
257257
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
258258
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
259-
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
260-
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
259+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
260+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
261261
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
262262
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
263263
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
264264
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
265265
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
266266
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
267-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
268-
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
269-
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
267+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
268+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
270269
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
271270
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
272271
github.com/tebeka/go2xunit v1.4.10/go.mod h1:wmc9jKT7KlU4QLU6DNTaIXNnYNOjKKNlp6mjOS0UrqY=
@@ -452,8 +451,9 @@ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGm
452451
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
453452
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
454453
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
455-
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
456454
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
455+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
456+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
457457
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
458458
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
459459
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=

internal/commands/all/all.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands/network"
1818
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands/networkpeering"
1919
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands/objectstorage"
20+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands/partner"
21+
partneraccount "github.com/UpCloudLtd/upcloud-cli/v3/internal/commands/partner/account"
2022
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands/root"
2123
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands/router"
2224
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands/server"
@@ -209,6 +211,12 @@ func BuildCommands(rootCmd *cobra.Command, conf *config.Config) {
209211
hostCommand := commands.BuildCommand(host.BaseHostCommand(), rootCmd, conf)
210212
commands.BuildCommand(host.ListCommand(), hostCommand.Cobra(), conf)
211213

214+
// Partner API
215+
partnerCommand := commands.BuildCommand(partner.BasePartnerCommand(), rootCmd, conf)
216+
partnerAccountCommand := commands.BuildCommand(partneraccount.BaseAccountCommand(), partnerCommand.Cobra(), conf)
217+
commands.BuildCommand(partneraccount.CreateCommand(), partnerAccountCommand.Cobra(), conf)
218+
commands.BuildCommand(partneraccount.ListCommand(), partnerAccountCommand.Cobra(), conf)
219+
212220
// Misc
213221
commands.BuildCommand(
214222
&root.VersionCommand{
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package partneraccount
2+
3+
import (
4+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
5+
)
6+
7+
// BaseAccountCommand creates the base "partner account" command
8+
func BaseAccountCommand() commands.Command {
9+
return &partnerAccountCommand{
10+
commands.New("account", "Manage accounts associated with partner"),
11+
}
12+
}
13+
14+
type partnerAccountCommand struct {
15+
*commands.BaseCommand
16+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package partneraccount
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
7+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/output"
8+
"github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/request"
9+
"github.com/spf13/pflag"
10+
)
11+
12+
type createCommand struct {
13+
*commands.BaseCommand
14+
params request.CreatePartnerAccountRequest
15+
}
16+
17+
func CreateCommand() commands.Command {
18+
return &createCommand{
19+
BaseCommand: commands.New(
20+
"create",
21+
"Create a new account that will be linked to partner's existing invoicing",
22+
"upctl partner account create --username newuser --password superSecret123",
23+
`upctl partner account create --username newuser --password superSecret123 --first-name New --last-name User --company "Example Ltd" --country FIN --phone +358.91111111 --email new.user@gmail.com`,
24+
),
25+
}
26+
}
27+
28+
func (s *createCommand) InitCommand() {
29+
fs := &pflag.FlagSet{}
30+
s.params.ContactDetails = &request.CreatePartnerAccountContactDetails{}
31+
cReqDesc := " Required when other contact details are given."
32+
33+
fs.StringVar(&s.params.Username, "username", "", "Account username.")
34+
fs.StringVar(&s.params.Password, "password", "", "Account pasword.")
35+
fs.StringVar(&s.params.ContactDetails.FirstName, "first-name", "", "Contact first name."+cReqDesc)
36+
fs.StringVar(&s.params.ContactDetails.LastName, "last-name", "", "Contact last name."+cReqDesc)
37+
fs.StringVar(&s.params.ContactDetails.Company, "company", "", "Contact company name.")
38+
fs.StringVar(&s.params.ContactDetails.Address, "address", "", "Contact street address.")
39+
fs.StringVar(&s.params.ContactDetails.PostalCode, "postal-code", "", "Contact postal/zip code.")
40+
fs.StringVar(&s.params.ContactDetails.City, "city", "", "Contact city.")
41+
fs.StringVar(&s.params.ContactDetails.State, "state", "", "Contact state. Required when other contact details are given and country is 'USA'.")
42+
fs.StringVar(&s.params.ContactDetails.Country, "country", "", "Contact ISO 3166-1 three character country code."+cReqDesc)
43+
fs.StringVar(&s.params.ContactDetails.Phone, "phone", "", "Contact phone number in international format, country code and national part separated by a period."+cReqDesc)
44+
fs.StringVar(&s.params.ContactDetails.Email, "email", "", "Contact email address."+cReqDesc)
45+
fs.StringVar(&s.params.ContactDetails.VATNumber, "vat-number", "", "Contact VAT number.")
46+
47+
s.AddFlags(fs)
48+
_ = s.Cobra().MarkFlagRequired("username")
49+
_ = s.Cobra().MarkFlagRequired("password")
50+
}
51+
52+
func (s *createCommand) ExecuteWithoutArguments(exec commands.Executor) (output.Output, error) {
53+
if (*s.params.ContactDetails == request.CreatePartnerAccountContactDetails{}) {
54+
s.params.ContactDetails = nil
55+
} else {
56+
cd := s.params.ContactDetails
57+
if cd.FirstName == "" || cd.LastName == "" || cd.Country == "" || cd.Phone == "" || cd.Email == "" {
58+
return nil, fmt.Errorf(`when contact details are given, the following flags are required: "first-name", "last-name", "country", "phone", "email"`)
59+
}
60+
if cd.Country == "USA" && cd.State == "" {
61+
return nil, fmt.Errorf(`when contact country is "USA", flag "state" is also required`)
62+
}
63+
}
64+
65+
msg := fmt.Sprintf("Creating account %s", s.params.Username)
66+
exec.PushProgressStarted(msg)
67+
68+
_, err := exec.All().CreatePartnerAccount(exec.Context(), &s.params)
69+
if err != nil {
70+
return commands.HandleError(exec, msg, err)
71+
}
72+
73+
exec.PushProgressSuccess(msg)
74+
75+
return output.None{}, nil
76+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package partneraccount
2+
3+
import (
4+
"testing"
5+
6+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
7+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/config"
8+
smock "github.com/UpCloudLtd/upcloud-cli/v3/internal/mock"
9+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/mockexecute"
10+
11+
"github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/request"
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestCreateCommand(t *testing.T) {
16+
targetMethod := "CreatePartnerAccount"
17+
password := "superSecret123"
18+
19+
generateArgs := func(namePhoneEmail bool, country string, state string, fullDetails bool) (args []string) {
20+
args = append(args,
21+
"--username", testPartnerAccount.Username,
22+
"--password", password,
23+
)
24+
if namePhoneEmail {
25+
args = append(args,
26+
"--first-name", testPartnerAccount.FirstName,
27+
"--last-name", testPartnerAccount.LastName,
28+
"--phone", testPartnerAccount.Phone,
29+
"--email", testPartnerAccount.Email,
30+
)
31+
}
32+
if country != "" {
33+
args = append(args, "--country", country)
34+
}
35+
if state != "" {
36+
args = append(args, "--state", state)
37+
} else if fullDetails {
38+
args = append(args,
39+
"--state", testPartnerAccount.State,
40+
"--company", testPartnerAccount.Company,
41+
"--address", testPartnerAccount.Address,
42+
"--postal-code", testPartnerAccount.PostalCode,
43+
"--city", testPartnerAccount.City,
44+
"--vat-number", testPartnerAccount.VATNumber,
45+
)
46+
}
47+
return
48+
}
49+
50+
generateRequest := func(country string, state string, fullDetails bool) (req request.CreatePartnerAccountRequest) {
51+
req.Username = testPartnerAccount.Username
52+
req.Password = password
53+
if country != "" {
54+
cd := &request.CreatePartnerAccountContactDetails{}
55+
cd.FirstName = testPartnerAccount.FirstName
56+
cd.LastName = testPartnerAccount.LastName
57+
cd.Country = country
58+
cd.Phone = testPartnerAccount.Phone
59+
cd.Email = testPartnerAccount.Email
60+
if state != "" {
61+
cd.State = state
62+
} else if fullDetails {
63+
cd.State = testPartnerAccount.State
64+
cd.Company = testPartnerAccount.Company
65+
cd.Address = testPartnerAccount.Address
66+
cd.PostalCode = testPartnerAccount.PostalCode
67+
cd.City = testPartnerAccount.City
68+
cd.VATNumber = testPartnerAccount.VATNumber
69+
}
70+
req.ContactDetails = cd
71+
}
72+
return
73+
}
74+
75+
for _, test := range []struct {
76+
name string
77+
args []string
78+
request request.CreatePartnerAccountRequest
79+
error string
80+
}{
81+
{
82+
name: "no args",
83+
args: []string{},
84+
error: `required flag(s) "username", "password" not set`,
85+
},
86+
{
87+
name: "no contact details",
88+
args: generateArgs(false, "", "", false),
89+
request: generateRequest("", "", false),
90+
},
91+
{
92+
name: "partial contact details",
93+
args: generateArgs(true, "", "", false),
94+
error: `when contact details are given, the following flags are required: "first-name", "last-name", "country", "phone", "email"`,
95+
},
96+
{
97+
name: "minimal contact details",
98+
args: generateArgs(true, testPartnerAccount.Country, "", false),
99+
request: generateRequest(testPartnerAccount.Country, "", false),
100+
},
101+
{
102+
name: "USA without a state",
103+
args: generateArgs(true, "USA", "", false),
104+
error: `when contact country is "USA", flag "state" is also required`,
105+
},
106+
{
107+
name: "USA with a state",
108+
args: generateArgs(true, "USA", "Florida", false),
109+
request: generateRequest("USA", "Florida", false),
110+
},
111+
{
112+
name: "full contact details",
113+
args: generateArgs(true, testPartnerAccount.Country, "", true),
114+
request: generateRequest(testPartnerAccount.Country, "", true),
115+
},
116+
} {
117+
t.Run(test.name, func(t *testing.T) {
118+
mService := smock.Service{}
119+
req := test.request
120+
mService.On(targetMethod, &req).Return(&testPartnerAccount, nil)
121+
122+
conf := config.New()
123+
command := commands.BuildCommand(CreateCommand(), nil, conf)
124+
command.Cobra().SetArgs(test.args)
125+
_, err := mockexecute.MockExecute(command, &mService, conf)
126+
127+
if test.error != "" {
128+
assert.EqualError(t, err, test.error)
129+
} else {
130+
assert.NoError(t, err)
131+
mService.AssertNumberOfCalls(t, targetMethod, 1)
132+
}
133+
})
134+
}
135+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package partneraccount
2+
3+
import (
4+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
5+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/output"
6+
)
7+
8+
func ListCommand() commands.Command {
9+
return &listCommand{
10+
BaseCommand: commands.New(
11+
"list",
12+
"List accounts associated with partner",
13+
"upctl partner account list",
14+
),
15+
}
16+
}
17+
18+
type listCommand struct {
19+
*commands.BaseCommand
20+
}
21+
22+
// ExecuteWithoutArguments implements commands.NoArgumentCommand
23+
func (l *listCommand) ExecuteWithoutArguments(exec commands.Executor) (output.Output, error) {
24+
accounts, err := exec.All().GetPartnerAccounts(exec.Context())
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
rows := []output.TableRow{}
30+
for _, a := range accounts {
31+
rows = append(rows, output.TableRow{
32+
a.Username,
33+
a.FirstName,
34+
a.LastName,
35+
a.Company,
36+
})
37+
}
38+
39+
return output.MarshaledWithHumanOutput{
40+
Value: accounts,
41+
Output: output.Table{
42+
Columns: []output.TableColumn{
43+
{Key: "username", Header: "Username"},
44+
{Key: "first_name", Header: "First name"},
45+
{Key: "last_name", Header: "Last name"},
46+
{Key: "company", Header: "Company"},
47+
},
48+
Rows: rows,
49+
},
50+
}, nil
51+
}

0 commit comments

Comments
 (0)