Skip to content

Commit 05758e8

Browse files
authored
feat(dbaas): add support for nested properties (#277)
1 parent 1c815b6 commit 05758e8

11 files changed

Lines changed: 200 additions & 10 deletions

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
- Support nested properties in `database properties *` and `database properties * show *` outputs. For example upctl `max_background_workers` sub-property of `timescaledb` PostgreSQL property is listed as `timescaledb.max_background_workers` in human output of `database properties pg` and its details can printed with `upctl database properties pg show timescaledb.max_background_workers` command.
12+
1013
## [3.2.1] - 2023-11-29
1114

1215
### Added

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.20
44

55
require (
66
github.com/UpCloudLtd/progress v1.0.2
7-
github.com/UpCloudLtd/upcloud-go-api/v6 v6.10.0
7+
github.com/UpCloudLtd/upcloud-go-api/v6 v6.11.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

go.sum

Lines changed: 2 additions & 2 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/v6 v6.10.0 h1:fs/hpOfuwpEHVxd+SbjNhcm3h+Rcv1S7zXqoVuIXcss=
21-
github.com/UpCloudLtd/upcloud-go-api/v6 v6.10.0/go.mod h1:I8rWmBBl+OhiY3AGzKbrobiE5TsLCLNYkCQxE4eJcTg=
20+
github.com/UpCloudLtd/upcloud-go-api/v6 v6.11.0 h1:8KvsimMoPPBx8IVebtHJHavrJPoJfNL5jsyW4TAC5m4=
21+
github.com/UpCloudLtd/upcloud-go-api/v6 v6.11.0/go.mod h1:I8rWmBBl+OhiY3AGzKbrobiE5TsLCLNYkCQxE4eJcTg=
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=

internal/commands/database/properties/format.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import (
88
)
99

1010
func formatAlternatives(val interface{}) (text.Colors, string, error) {
11+
return formatStringSlice(val, "or")
12+
}
13+
14+
func formatProperties(val interface{}) (text.Colors, string, error) {
15+
return formatStringSlice(val, "and")
16+
}
17+
18+
func formatStringSlice(val interface{}, andOrOr string) (text.Colors, string, error) {
1119
if val == nil {
1220
return nil, "", nil
1321
}
@@ -17,7 +25,15 @@ func formatAlternatives(val interface{}) (text.Colors, string, error) {
1725
}
1826

1927
if ifaceSliceVal, ok := val.([]interface{}); ok {
20-
return nil, alternativesString(ifaceSliceVal), nil
28+
return nil, alternativesString(ifaceSliceVal, andOrOr), nil
29+
}
30+
31+
if stringSliceVal, ok := val.([]string); ok {
32+
ifaceSliceVal := []interface{}{}
33+
for _, i := range stringSliceVal {
34+
ifaceSliceVal = append(ifaceSliceVal, i)
35+
}
36+
return nil, alternativesString(ifaceSliceVal, andOrOr), nil
2137
}
2238

2339
return nil, fmt.Sprintf("%+v", val), nil
@@ -33,7 +49,7 @@ func maxStringLen(strings []string) int {
3349
return max
3450
}
3551

36-
func alternativesString(values []interface{}) string {
52+
func alternativesString(values []interface{}, andOrOr string) string {
3753
if len(values) == 0 {
3854
return ""
3955
}
@@ -53,5 +69,5 @@ func alternativesString(values []interface{}) string {
5369
}
5470

5571
str := strings.Join(strs[:len(strs)-1], ","+whitespace)
56-
return str + fmt.Sprintf(" or%s%s", whitespace, strs[len(values)-1])
72+
return str + fmt.Sprintf(" %s%s%s", andOrOr, whitespace, strs[len(values)-1])
5773
}

internal/commands/database/properties/show.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package databaseproperties
22

33
import (
44
"fmt"
5+
"sort"
56

67
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
78
"github.com/UpCloudLtd/upcloud-cli/v3/internal/completion"
89
"github.com/UpCloudLtd/upcloud-cli/v3/internal/format"
910
"github.com/UpCloudLtd/upcloud-cli/v3/internal/output"
11+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/utils"
1012
"github.com/UpCloudLtd/upcloud-go-api/v6/upcloud/request"
1113
)
1214

@@ -33,11 +35,18 @@ func (s *showCommand) Execute(exec commands.Executor, key string) (output.Output
3335
return nil, err
3436
}
3537

36-
details, ok := dbType.Properties[key]
38+
flatProperties := utils.GetFlatDatabaseProperties(dbType.Properties)
39+
details, ok := flatProperties[key]
3740
if !ok {
3841
return nil, fmt.Errorf(`no property "%s" available for %s database`, key, s.serviceType)
3942
}
4043

44+
childProperties := []string{}
45+
for key := range details.Properties {
46+
childProperties = append(childProperties, key)
47+
}
48+
sort.Strings(childProperties)
49+
4150
rows := []output.DetailRow{
4251
{Title: "Key:", Key: "key", Value: key},
4352
{Title: "Title:", Key: "title", Value: details.Title},
@@ -48,8 +57,11 @@ func (s *showCommand) Execute(exec commands.Executor, key string) (output.Output
4857
{Title: "Default:", Key: "default", Value: details.Default},
4958
{Title: "Possible values:", Key: "enum", Value: details.Enum, Format: formatAlternatives},
5059
{Title: "Pattern:", Key: "pattern", Value: details.Pattern},
51-
{Title: "Max length:", Key: "maxLength", Value: details.MaxLength},
5260
{Title: "Min length:", Key: "minLength", Value: details.MinLength},
61+
{Title: "Minimum:", Key: "minimum", Value: details.Minimum, Format: format.Dereference[float64]},
62+
{Title: "Max length:", Key: "maxLength", Value: details.MaxLength},
63+
{Title: "Maximum:", Key: "maximum", Value: details.Maximum, Format: format.Dereference[float64]},
64+
{Title: "Properties:", Key: "properties", Value: childProperties, Format: formatProperties},
5365
}
5466

5567
return output.MarshaledWithHumanOutput{
@@ -75,10 +87,18 @@ func filterOutEmptyRows(rows []output.DetailRow) []output.DetailRow {
7587
continue
7688
}
7789

90+
if val, ok := row.Value.([]string); ok && len(val) == 0 {
91+
continue
92+
}
93+
7894
if val, ok := row.Value.(int); ok && val == 0 {
7995
continue
8096
}
8197

98+
if val, ok := row.Value.(*float64); ok && val == nil {
99+
continue
100+
}
101+
82102
nonEmpty = append(nonEmpty, row)
83103
}
84104

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package databaseproperties
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/jedib0t/go-pretty/v6/text"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/mock"
14+
)
15+
16+
func TestDatabasePropertiesShow(t *testing.T) {
17+
text.DisableColors()
18+
19+
mService := smock.Service{}
20+
mService.On("GetManagedDatabaseServiceType", mock.Anything).Return(&propertiesTestdata, nil)
21+
22+
conf := config.New()
23+
// force human output
24+
conf.Viper().Set(config.KeyOutput, config.ValueOutputHuman)
25+
26+
command := commands.BuildCommand(ShowCommand("pg", "PostgreSQL"), nil, conf)
27+
28+
command.Cobra().SetArgs([]string{"timescaledb.max_background_workers"})
29+
output, err := mockexecute.MockExecute(command, &mService, conf)
30+
31+
assert.NoError(t, err)
32+
33+
expected := `
34+
Key: timescaledb.max_background_workers
35+
Title: timescaledb.max_background_workers
36+
Description: The number of background workers for timescaledb operations. You should configure this setting to the sum of your number of databases and the total number of concurrent background workers you want running at any given point in time.
37+
Create only: no
38+
Type: integer
39+
Default: 16
40+
Minimum: 1
41+
Maximum: 4096
42+
43+
`
44+
assert.Equal(t, expected, output)
45+
}

internal/commands/database/properties/type.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
88
"github.com/UpCloudLtd/upcloud-cli/v3/internal/format"
99
"github.com/UpCloudLtd/upcloud-cli/v3/internal/output"
10+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/utils"
1011
"github.com/UpCloudLtd/upcloud-go-api/v6/upcloud/request"
1112
)
1213

@@ -33,7 +34,7 @@ func (s *dbTypeCommand) ExecuteWithoutArguments(exec commands.Executor) (output.
3334

3435
properties := dbType.Properties
3536
rows := []output.TableRow{}
36-
for key, details := range properties {
37+
for key, details := range utils.GetFlatDatabaseProperties(properties) {
3738
enumOrExample := details.Enum
3839
if enumOrExample == nil {
3940
enumOrExample = details.Example
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package databaseproperties
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+
"github.com/UpCloudLtd/upcloud-go-api/v6/upcloud"
11+
12+
"github.com/jedib0t/go-pretty/v6/text"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/mock"
15+
)
16+
17+
var propertiesTestdata = upcloud.ManagedDatabaseType{
18+
Properties: map[string]upcloud.ManagedDatabaseServiceProperty{
19+
"timescaledb": {
20+
Title: "TimescaleDB extension configuration values",
21+
Description: "System-wide settings for the timescaledb extension",
22+
Type: "object",
23+
Properties: map[string]upcloud.ManagedDatabaseServiceProperty{
24+
"max_background_workers": {
25+
Default: 16.0,
26+
Example: 8.0,
27+
Title: "timescaledb.max_background_workers",
28+
Type: "integer",
29+
Description: "The number of background workers for timescaledb operations. You should configure this setting to the sum of your number of databases and the total number of concurrent background workers you want running at any given point in time.",
30+
Minimum: upcloud.Float64Ptr(1),
31+
Maximum: upcloud.Float64Ptr(4096),
32+
},
33+
},
34+
},
35+
},
36+
}
37+
38+
func TestDatabasePropertiesByType(t *testing.T) {
39+
text.DisableColors()
40+
41+
mService := smock.Service{}
42+
mService.On("GetManagedDatabaseServiceType", mock.Anything).Return(&propertiesTestdata, nil)
43+
44+
conf := config.New()
45+
// force human output
46+
conf.Viper().Set(config.KeyOutput, config.ValueOutputHuman)
47+
48+
command := commands.BuildCommand(DBTypeCommand("pg", "PostgreSQL"), nil, conf)
49+
50+
output, err := mockexecute.MockExecute(command, &mService, conf)
51+
52+
assert.NoError(t, err)
53+
54+
expected := `
55+
Property Create only Type Example
56+
──────────────────────────────────── ───────────── ───────── ─────────
57+
timescaledb no object
58+
timescaledb.max_background_workers no integer 8
59+
60+
`
61+
assert.Equal(t, expected, output)
62+
}

internal/completion/database.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66

77
"github.com/UpCloudLtd/upcloud-cli/v3/internal/service"
8+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/utils"
89
"github.com/UpCloudLtd/upcloud-go-api/v6/upcloud/request"
910
"github.com/spf13/cobra"
1011
)
@@ -62,7 +63,7 @@ func (s DatabaseProperty) CompleteArgument(ctx context.Context, svc service.AllS
6263
return None(toComplete)
6364
}
6465

65-
properties := dbType.Properties
66+
properties := utils.GetFlatDatabaseProperties(dbType.Properties)
6667
var vals []string
6768
for key, details := range properties {
6869
description := details.Title

internal/format/common.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ func Boolean(val interface{}) (text.Colors, string, error) {
2727
return nil, "", fmt.Errorf("cannot parse '%v' (%T) as boolean", val, val)
2828
}
2929

30+
// Dereference returns "%v" Sprintf'ed value of a pointer
31+
func Dereference[T any](val interface{}) (text.Colors, string, error) {
32+
ptr, ok := val.(*T)
33+
if !ok {
34+
return nil, "", fmt.Errorf("cannot parse %T, expected pointer", val)
35+
}
36+
37+
if ptr != nil {
38+
return nil, fmt.Sprintf("%v", *ptr), nil
39+
}
40+
41+
return text.Colors{text.FgHiBlack}, "nil", nil
42+
}
43+
3044
// PossiblyUnknownString outputs "Unknown" in light black if input value is an empty string, otherwise passesthrough the input value.
3145
func PossiblyUnknownString(val interface{}) (text.Colors, string, error) {
3246
str, ok := val.(string)

0 commit comments

Comments
 (0)