Skip to content

Commit 93c3b64

Browse files
authored
Merge branch 'UpCloudLtd:main' into feat/billing-summary
2 parents 9046567 + 6770279 commit 93c3b64

File tree

10 files changed

+214
-20
lines changed

10 files changed

+214
-20
lines changed

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
id: release_notes
4747
run: make release-notes > .release_notes
4848
- name: Set up Syft
49-
uses: anchore/sbom-action/download-syft@43a17d6e7add2b5535efe4dcae9952337c479a93 # v0.20.11
49+
uses: anchore/sbom-action/download-syft@a930d0ac434e3182448fe678398ba5713717112a # v0.21.0
5050
- name: Run goreleaser-action
5151
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
5252
with:
@@ -58,7 +58,7 @@ jobs:
5858
- name: Clear Docker login session
5959
run: rm -f ${HOME}/.docker/config.json
6060
- name: Generate artifact attestations
61-
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
61+
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
6262
with:
6363
subject-checksums: dist/checksums.txt
6464
- name: Generate AUR PKGBUILD

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [3.27.0] - 2025-12-31
11+
1012
### Added
1113

1214
- In `kubernetes create`, allow waiting for cluster and its node-groups to reach running state with `--wait=all` flag. When using `--wait` or `--wait=cluster`, the command will wait only for the cluster to reach running state.
15+
- Add `account billing` command for listing billing details.
16+
- Add support for Valkey to `database show` and `database session list` commands.
17+
18+
### Removed
19+
20+
- Removed support for deprecated Redis service. This is treated as non-breaking change as all users have been migrated to Valkey.
1321

1422
## [3.26.0] - 2025-11-26
1523

@@ -652,7 +660,8 @@ Initial public beta release :tada:
652660
### Added
653661
- Current feature set added! First internal release
654662
655-
[Unreleased]: https://github.com/UpCloudLtd/upcloud-cli/compare/v3.26.0...HEAD
663+
[Unreleased]: https://github.com/UpCloudLtd/upcloud-cli/compare/v3.27.0...HEAD
664+
[3.27.0]: https://github.com/UpCloudLtd/upcloud-cli/compare/v3.26.0...v3.27.0
656665
[3.26.0]: https://github.com/UpCloudLtd/upcloud-cli/compare/v3.25.0...v3.26.0
657666
[3.25.0]: https://github.com/UpCloudLtd/upcloud-cli/compare/v3.24.1...v3.25.0
658667
[3.24.1]: https://github.com/UpCloudLtd/upcloud-cli/compare/v3.24.0...v3.24.1

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ go 1.25.3
44

55
require (
66
dario.cat/mergo v1.0.2
7-
github.com/UpCloudLtd/progress v1.0.3
7+
github.com/UpCloudLtd/progress v1.1.0
88
github.com/UpCloudLtd/upcloud-go-api/credentials v0.1.1
9-
github.com/UpCloudLtd/upcloud-go-api/v8 v8.32.0
9+
github.com/UpCloudLtd/upcloud-go-api/v8 v8.33.0
1010
github.com/adrg/xdg v0.5.3
1111
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
1212
github.com/gemalto/flume v1.0.0
13-
github.com/jedib0t/go-pretty/v6 v6.7.7
13+
github.com/jedib0t/go-pretty/v6 v6.7.8
1414
github.com/joho/godotenv v1.5.1
1515
github.com/m7shapan/cidr v0.0.0-20200427124835-7eba0889a5d2
1616
github.com/mattn/go-isatty v0.0.20

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -419,12 +419,12 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC
419419
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
420420
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
421421
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
422-
github.com/UpCloudLtd/progress v1.0.3 h1:8SfntHkBPyQc5BL3946Bgi9KYnQOxa5RR2EKdadujdg=
423-
github.com/UpCloudLtd/progress v1.0.3/go.mod h1:iGxOnb9HvHW0yrLGUjHr0lxHhn7TehgWwh7a8NqK6iQ=
422+
github.com/UpCloudLtd/progress v1.1.0 h1:RT2DNMlvJy1R8WZr2dKdDgR+xD1+3+UdZauIfWr2q2w=
423+
github.com/UpCloudLtd/progress v1.1.0/go.mod h1:iGxOnb9HvHW0yrLGUjHr0lxHhn7TehgWwh7a8NqK6iQ=
424424
github.com/UpCloudLtd/upcloud-go-api/credentials v0.1.1 h1:eTfQsv58ufALOk9BZ7WbS/i7pMUD11RnYYpRPsz0LdI=
425425
github.com/UpCloudLtd/upcloud-go-api/credentials v0.1.1/go.mod h1:7OtVs2UqtfvjkC1HfE+Oud0MnbMv7qUWnbEgxnTAqts=
426-
github.com/UpCloudLtd/upcloud-go-api/v8 v8.32.0 h1:mtxrTUWr7vB2lcv0KEpHpXAdQMl4ol024I+P+qfEIRc=
427-
github.com/UpCloudLtd/upcloud-go-api/v8 v8.32.0/go.mod h1:NBh1d/ip1bhdAIhuPWbyPme7tbLzDTV7dhutUmU1vg8=
426+
github.com/UpCloudLtd/upcloud-go-api/v8 v8.33.0 h1:18MDgUePzdapCSKwLWVL+WRawyT25yMxmV9TQ32KQJQ=
427+
github.com/UpCloudLtd/upcloud-go-api/v8 v8.33.0/go.mod h1:NBh1d/ip1bhdAIhuPWbyPme7tbLzDTV7dhutUmU1vg8=
428428
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
429429
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
430430
github.com/ansel1/merry v1.5.0/go.mod h1:wUy/yW0JX0ix9GYvUbciq+bi3jW/vlKPlbpI7qdZpOw=
@@ -722,8 +722,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
722722
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
723723
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
724724
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
725-
github.com/jedib0t/go-pretty/v6 v6.7.7 h1:Y1Id3lJ3k4UB8uwWWy3l8EVFnUlx5chR5+VbsofPNX0=
726-
github.com/jedib0t/go-pretty/v6 v6.7.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
725+
github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o=
726+
github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
727727
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
728728
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
729729
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package account
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
7+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
8+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/output"
9+
"github.com/UpCloudLtd/upcloud-go-api/v8/upcloud"
10+
"github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/request"
11+
"github.com/spf13/pflag"
12+
)
13+
14+
// BillingCommand creates the 'account billing' command
15+
func BillingCommand() commands.Command {
16+
return &billingCommand{
17+
BaseCommand: commands.New(
18+
"billing",
19+
"Show billing information",
20+
"upctl account billing --year 2025 --month 7",
21+
),
22+
}
23+
}
24+
25+
type billingCommand struct {
26+
*commands.BaseCommand
27+
year int
28+
month int
29+
resourceID string
30+
username string
31+
}
32+
33+
// InitCommand implements Command.InitCommand
34+
func (s *billingCommand) InitCommand() {
35+
flagSet := &pflag.FlagSet{}
36+
37+
flagSet.IntVar(&s.year, "year", 0, "Year for billing information.")
38+
flagSet.IntVar(&s.month, "month", 0, "Month for billing information.")
39+
flagSet.StringVar(&s.resourceID, "resource-id", "", "For IP addresses: the address itself, others, resource UUID")
40+
flagSet.StringVar(&s.username, "username", "", "Valid username")
41+
42+
s.AddFlags(flagSet)
43+
44+
commands.Must(s.Cobra().MarkFlagRequired("year"))
45+
commands.Must(s.Cobra().MarkFlagRequired("month"))
46+
}
47+
48+
func firstElementAsString(row output.TableRow) string {
49+
s, ok := row[0].(string)
50+
if !ok {
51+
return ""
52+
}
53+
return s
54+
}
55+
56+
// ExecuteWithoutArguments implements commands.NoArgumentCommand
57+
func (s *billingCommand) ExecuteWithoutArguments(exec commands.Executor) (output.Output, error) {
58+
if s.year < 1900 || s.year > 9999 {
59+
return nil, fmt.Errorf("invalid year: %d", s.year)
60+
}
61+
if s.month < 1 || s.month > 12 {
62+
return nil, fmt.Errorf("invalid month: %d", s.month)
63+
}
64+
65+
svc := exec.Account()
66+
summary, err := svc.GetBillingSummary(exec.Context(), &request.GetBillingSummaryRequest{
67+
YearMonth: fmt.Sprintf("%d-%02d", s.year, s.month),
68+
ResourceID: s.resourceID,
69+
Username: s.username,
70+
})
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
createCategorySections := func() []output.CombinedSection {
76+
var sections []output.CombinedSection
77+
var summaryRows []output.TableRow
78+
79+
categories := map[string]*upcloud.BillingCategory{
80+
"Servers": summary.Servers,
81+
"Managed Databases": summary.ManagedDatabases,
82+
"Managed Object Storages": summary.ManagedObjectStorages,
83+
"Managed Load Balancers": summary.ManagedLoadbalancers,
84+
"Managed Kubernetes": summary.ManagedKubernetes,
85+
"Network Gateways": summary.NetworkGateways,
86+
"Networks": summary.Networks,
87+
"Storages": summary.Storages,
88+
}
89+
90+
for categoryName, category := range categories {
91+
if category != nil {
92+
summaryRows = append(summaryRows, output.TableRow{categoryName, category.TotalAmount})
93+
resourceGroups := map[string]*upcloud.BillingResourceGroup{
94+
"Server": category.Server,
95+
"Managed Database": category.ManagedDatabase,
96+
"Managed Object Storage": category.ManagedObjectStorage,
97+
"Managed Load Balancer": category.ManagedLoadbalancer,
98+
"Managed Kubernetes": category.ManagedKubernetes,
99+
"Network Gateway": category.NetworkGateway,
100+
"IPv4 Address": category.IPv4Address,
101+
"Backup": category.Backup,
102+
"Storage": category.Storage,
103+
"Template": category.Template,
104+
}
105+
106+
for groupName, group := range resourceGroups {
107+
if group != nil && len(group.Resources) > 0 {
108+
var resourceRows []output.TableRow
109+
for _, resource := range group.Resources {
110+
resourceRows = append(resourceRows, output.TableRow{
111+
resource.ResourceID,
112+
resource.Amount,
113+
resource.Hours,
114+
})
115+
}
116+
117+
sections = append(sections, output.CombinedSection{
118+
Key: fmt.Sprintf("%s_%s_resources", categoryName, groupName),
119+
Title: fmt.Sprintf("%s - %s Resources:", categoryName, groupName),
120+
Contents: output.Table{
121+
Columns: []output.TableColumn{
122+
{Key: "resource_id", Header: "Resource ID"},
123+
{Key: "amount", Header: "Amount"},
124+
{Key: "hours", Header: "Hours"},
125+
},
126+
Rows: resourceRows,
127+
EmptyMessage: fmt.Sprintf("No resources for %s.", groupName),
128+
},
129+
})
130+
}
131+
}
132+
}
133+
}
134+
135+
sort.Slice(summaryRows, func(i, j int) bool {
136+
return firstElementAsString(summaryRows[i]) < firstElementAsString(summaryRows[j])
137+
})
138+
summaryRows = append(summaryRows, output.TableRow{"Total", summary.TotalAmount})
139+
140+
sort.Slice(sections, func(i, j int) bool {
141+
return sections[i].Title < sections[j].Title
142+
})
143+
sections = append([]output.CombinedSection{{
144+
Key: "summary",
145+
Title: "Summary:",
146+
Contents: output.Table{
147+
Columns: []output.TableColumn{
148+
{Key: "resource", Header: "Resource"},
149+
{Key: "total_amount", Header: "Amount"},
150+
},
151+
Rows: summaryRows,
152+
},
153+
}}, sections...)
154+
return sections
155+
}
156+
157+
combined := output.Combined(createCategorySections())
158+
159+
return output.MarshaledWithHumanOutput{
160+
Value: summary,
161+
Output: combined,
162+
}, nil
163+
}

internal/commands/base/base.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ func BuildCommands(rootCmd *cobra.Command, conf *config.Config) {
133133
commands.BuildCommand(account.ShowCommand(), accountCommand.Cobra(), conf)
134134
commands.BuildCommand(account.ListCommand(), accountCommand.Cobra(), conf)
135135
commands.BuildCommand(account.DeleteCommand(), accountCommand.Cobra(), conf)
136+
commands.BuildCommand(account.BillingCommand(), accountCommand.Cobra(), conf)
136137

137138
// Account permissions
138139
permissionsCommand := commands.BuildCommand(permissions.BasePermissionsCommand(), accountCommand.Cobra(), conf)
@@ -181,7 +182,6 @@ func BuildCommands(rootCmd *cobra.Command, conf *config.Config) {
181182
{serviceName: "MySQL", serviceType: "mysql"},
182183
{serviceName: "OpenSearch", serviceType: "opensearch"},
183184
{serviceName: "PostgreSQL", serviceType: "pg"},
184-
{serviceName: "Redis", serviceType: "redis"},
185185
{serviceName: "Valkey", serviceType: "valkey"},
186186
} {
187187
typeCommand := commands.BuildCommand(databaseproperties.DBTypeCommand(i.serviceType, i.serviceName), propertiesCommand.Cobra(), conf)
@@ -288,7 +288,6 @@ func BuildCommands(rootCmd *cobra.Command, conf *config.Config) {
288288
allCommand := commands.BuildCommand(all.BaseAllCommand(), rootCmd, conf)
289289
commands.BuildCommand(all.PurgeCommand(), allCommand.Cobra(), conf)
290290
commands.BuildCommand(all.ListCommand(), allCommand.Cobra(), conf)
291-
292291
// Stack operations
293292
stackCommand := commands.BuildCommand(stack.BaseStackCommand(), rootCmd, conf)
294293
stackDeployCommand := commands.BuildCommand(stack.DeployCommand(), stackCommand.Cobra(), conf)

internal/commands/database/session/list.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ func (s *listCommand) Execute(exec commands.Executor, uuid string) (output.Outpu
6565
outputFn = mysql
6666
case upcloud.ManagedDatabaseServiceTypePostgreSQL:
6767
outputFn = pg
68-
case upcloud.ManagedDatabaseServiceTypeRedis: //nolint:staticcheck // To be removed when Redis support has been removed
69-
outputFn = redis
68+
case upcloud.ManagedDatabaseServiceTypeValkey:
69+
outputFn = valkey
7070
default:
7171
return nil, fmt.Errorf("session list not supported for database type %s", db.Type)
7272
}
@@ -156,10 +156,10 @@ func pg(sessions upcloud.ManagedDatabaseSessions) output.Output {
156156
}
157157
}
158158

159-
func redis(sessions upcloud.ManagedDatabaseSessions) output.Output {
159+
func valkey(sessions upcloud.ManagedDatabaseSessions) output.Output {
160160
rows := make([]output.TableRow, 0)
161161

162-
for _, session := range sessions.Redis { //nolint:staticcheck // To be removed when Redis support has been removed
162+
for _, session := range sessions.Valkey {
163163
rows = append(rows, output.TableRow{
164164
session.Id,
165165
session.Query,

internal/commands/database/show.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ func getVersion(db *upcloud.ManagedDatabase) string {
160160
return db.Metadata.OpenSearchVersion
161161
case "pg":
162162
return db.Metadata.PGVersion
163-
case "redis":
164-
return db.Metadata.RedisVersion //nolint:staticcheck // To be removed when Redis support has been removed
163+
case "valkey":
164+
return db.Metadata.ValkeyVersion
165165
}
166166
return ""
167167
}

internal/commands/database/show_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ func TestGetVersion(t *testing.T) {
2828
Type: upcloud.ManagedDatabaseServiceTypePostgreSQL,
2929
Metadata: &upcloud.ManagedDatabaseMetadata{PGVersion: "15"},
3030
},
31+
}, {
32+
name: "valkey",
33+
expected: "8.1.5",
34+
db: &upcloud.ManagedDatabase{
35+
Type: upcloud.ManagedDatabaseServiceTypeValkey,
36+
Metadata: &upcloud.ManagedDatabaseMetadata{ValkeyVersion: "8.1.5"},
37+
},
3138
},
3239
} {
3340
t.Run(test.name, func(t *testing.T) {

internal/mock/mock.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,6 +1312,22 @@ func (m *Service) DetachManagedObjectStorageUserPolicy(ctx context.Context, r *r
13121312
return nil
13131313
}
13141314

1315+
func (m *Service) CreateManagedObjectStoragePolicyVersion(ctx context.Context, r *request.CreateManagedObjectStoragePolicyVersionRequest) (*upcloud.ManagedObjectStoragePolicyVersion, error) {
1316+
return nil, nil
1317+
}
1318+
1319+
func (m *Service) GetManagedObjectStoragePolicyVersion(ctx context.Context, r *request.GetManagedObjectStoragePolicyVersionRequest) (*upcloud.ManagedObjectStoragePolicyVersion, error) {
1320+
return nil, nil
1321+
}
1322+
1323+
func (m *Service) GetManagedObjectStoragePolicyVersions(ctx context.Context, r *request.GetManagedObjectStoragePolicyVersionsRequest) ([]upcloud.ManagedObjectStoragePolicyVersion, error) {
1324+
return nil, nil
1325+
}
1326+
1327+
func (m *Service) DeleteManagedObjectStoragePolicyVersion(ctx context.Context, r *request.DeleteManagedObjectStoragePolicyVersionRequest) error {
1328+
return nil
1329+
}
1330+
13151331
func (m *Service) CreateManagedObjectStorageCustomDomain(ctx context.Context, r *request.CreateManagedObjectStorageCustomDomainRequest) error {
13161332
return m.Called(r).Error(0)
13171333
}

0 commit comments

Comments
 (0)