Skip to content

Commit 7d1069a

Browse files
committed
feat(database): add database show command
1 parent c6a4b9a commit 7d1069a

5 files changed

Lines changed: 216 additions & 18 deletions

File tree

internal/commands/all/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ func BuildCommands(rootCmd *cobra.Command, conf *config.Config) {
102102
// Databases
103103
databaseCommand := commands.BuildCommand(database.BaseDatabaseCommand(), rootCmd, conf)
104104
commands.BuildCommand(database.ListCommand(), databaseCommand.Cobra(), conf)
105+
commands.BuildCommand(database.ShowCommand(), databaseCommand.Cobra(), conf)
105106

106107
// Misc
107108
commands.BuildCommand(

internal/commands/database/show.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package database
2+
3+
import (
4+
"github.com/UpCloudLtd/upcloud-cli/internal/commands"
5+
"github.com/UpCloudLtd/upcloud-cli/internal/output"
6+
"github.com/UpCloudLtd/upcloud-cli/internal/ui"
7+
"github.com/UpCloudLtd/upcloud-go-api/v4/upcloud"
8+
"github.com/UpCloudLtd/upcloud-go-api/v4/upcloud/request"
9+
)
10+
11+
// ShowCommand creates the "database show" command
12+
func ShowCommand() commands.Command {
13+
return &showCommand{
14+
BaseCommand: commands.New(
15+
"show",
16+
"Show database details",
17+
"upctl database show 9a8effcb-80e6-4a63-a7e5-066a6d093c14",
18+
"upctl database show my-pg-database",
19+
"upctl database show my-mysql-database",
20+
),
21+
}
22+
}
23+
24+
type showCommand struct {
25+
*commands.BaseCommand
26+
}
27+
28+
// Execute implements commands.MultipleArgumentCommand
29+
func (s *showCommand) Execute(exec commands.Executor, uuid string) (output.Output, error) {
30+
svc := exec.All()
31+
db, err := svc.GetManagedDatabase(&request.GetManagedDatabaseRequest{UUID: uuid})
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
nodeRows := []output.TableRow{}
37+
for _, node := range db.NodeStates {
38+
nodeRows = append(nodeRows, output.TableRow{
39+
node.Name,
40+
node.Role,
41+
node.State,
42+
})
43+
}
44+
45+
componentsRows := []output.TableRow{}
46+
for _, component := range db.Components {
47+
componentsRows = append(componentsRows, output.TableRow{
48+
component.Component,
49+
component.Host,
50+
component.Port,
51+
component.Route,
52+
component.Usage,
53+
})
54+
}
55+
56+
// For JSON and YAML output, passthrough API response
57+
return output.MarshaledWithHumanOutput{
58+
Value: db,
59+
Output: output.Combined{
60+
output.CombinedSection{
61+
Contents: output.Details{
62+
Sections: []output.DetailSection{
63+
{
64+
Title: "Overview:",
65+
Rows: []output.DetailRow{
66+
{Title: "UUID:", Value: db.UUID, Colour: ui.DefaultUUUIDColours},
67+
{Title: "Title:", Value: db.Title},
68+
{Title: "Name:", Value: db.Name},
69+
{Title: "Type:", Value: prettyDatabaseType(db.Type)},
70+
{Title: "Version:", Value: db.Properties.Get("version")},
71+
{Title: "Plan:", Value: db.Plan},
72+
{Title: "Zone:", Value: db.Zone},
73+
{Title: "State:", Value: db.State, Colour: commands.DatabaseStateColour(db.State)},
74+
},
75+
},
76+
{
77+
Title: "Maintenance schedule:",
78+
Rows: []output.DetailRow{
79+
{Title: "Weekday:", Value: db.Maintenance.DayOfWeek},
80+
{Title: "Time:", Value: db.Maintenance.Time},
81+
},
82+
},
83+
{
84+
Title: "Authentication:",
85+
Rows: []output.DetailRow{
86+
{Title: "Service URI:", Value: db.ServiceURI},
87+
{Title: "Database name:", Value: db.ServiceURIParams.DatabaseName},
88+
{Title: "Host:", Value: db.ServiceURIParams.Host},
89+
{Title: "Password:", Value: db.ServiceURIParams.Password},
90+
{Title: "Port:", Value: db.ServiceURIParams.Port},
91+
{Title: "SSL mode:", Value: db.ServiceURIParams.SSLMode},
92+
{Title: "User:", Value: db.ServiceURIParams.User},
93+
},
94+
},
95+
},
96+
},
97+
},
98+
output.CombinedSection{
99+
Title: "Nodes:",
100+
Contents: output.Table{
101+
Columns: []output.TableColumn{
102+
{Key: "name", Header: "Name"},
103+
{Key: "role", Header: "Type"},
104+
{Key: "state", Header: "State"},
105+
},
106+
Rows: nodeRows,
107+
},
108+
},
109+
output.CombinedSection{
110+
Title: "Components:",
111+
Contents: output.Table{
112+
Columns: []output.TableColumn{
113+
{Key: "component", Header: "Component"},
114+
{Key: "host", Header: "Host"},
115+
{Key: "port", Header: "Port"},
116+
{Key: "route", Header: "Route"},
117+
{Key: "usage", Header: "Usage"},
118+
},
119+
Rows: componentsRows,
120+
},
121+
},
122+
},
123+
}, nil
124+
}
125+
126+
func prettyDatabaseType(serviceType upcloud.ManagedDatabaseServiceType) string {
127+
switch serviceType {
128+
case upcloud.ManagedDatabaseServiceTypeMySQL:
129+
return "MySQL"
130+
case upcloud.ManagedDatabaseServiceTypePostgreSQL:
131+
return "PostgreSQL"
132+
default:
133+
return string(serviceType)
134+
}
135+
}
Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,24 @@
11
package output
22

33
import (
4-
"encoding/json"
54
"errors"
6-
7-
"gopkg.in/yaml.v2"
85
)
96

10-
// MarshaledWithHumanDetails implements output.Command for a return value that is only displayed as raw marshaled in JSON and YAML
11-
// eg. most 'state change' commands
7+
// MarshaledWithHumanDetails implements output.Command for a return value that is displayed as raw marshaled in JSON and YAML or as human details, such as UUID or IP address, eg. most create commands.
128
type MarshaledWithHumanDetails struct {
139
Value interface{}
1410
Details []DetailRow
1511
}
1612

1713
// MarshalJSON implements json.Marshaler and output.Output
1814
func (d MarshaledWithHumanDetails) MarshalJSON() ([]byte, error) {
19-
if errValue, ok := d.Value.(error); ok {
20-
return json.MarshalIndent(map[string]interface{}{
21-
"error": errValue.Error(),
22-
}, "", " ")
23-
}
24-
return json.MarshalIndent(d.Value, "", " ")
15+
return OnlyMarshaled{Value: d.Value}.MarshalJSON()
2516
}
2617

2718
// MarshalYAML implements output.Output, it marshals the value and returns the YAML as []byte
2819
// nb. does *not* implement yaml.Marshaler
2920
func (d MarshaledWithHumanDetails) MarshalYAML() ([]byte, error) {
30-
if errValue, ok := d.Value.(error); ok {
31-
return yaml.Marshal(map[string]interface{}{
32-
"error": errValue.Error(),
33-
})
34-
}
35-
return yaml.Marshal(d.Value)
21+
return OnlyMarshaled{Value: d.Value}.MarshalYAML()
3622
}
3723

3824
// MarshalHuman implements output.Output
@@ -45,5 +31,5 @@ func (d MarshaledWithHumanDetails) MarshalHuman() ([]byte, error) {
4531

4632
// MarshalRawMap implements output.Output
4733
func (d MarshaledWithHumanDetails) MarshalRawMap() (map[string]interface{}, error) {
48-
return nil, errors.New("marshaledwithhumandetails output should not be used as part of multiple output, raw output is undefined")
34+
return nil, errors.New("MarshaledWithHumanDetails output should not be used as part of multiple output, raw output is undefined")
4935
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package output
2+
3+
import (
4+
"errors"
5+
6+
"gopkg.in/yaml.v2"
7+
)
8+
9+
// MarshaledWithHumanOutput implements output.Command for a return value that passes through raw marshaled JSON or YAML and only affects human output. Like MarshaledWithHumanDetails, but allows more complex output.
10+
type MarshaledWithHumanOutput struct {
11+
Value interface{}
12+
Output Output
13+
}
14+
15+
// MarshalJSON implements json.Marshaler and output.Output
16+
func (d MarshaledWithHumanOutput) MarshalJSON() ([]byte, error) {
17+
return OnlyMarshaled{Value: d.Value}.MarshalJSON()
18+
}
19+
20+
// MarshalYAML implements output.Output, it marshals the value and returns the YAML as []byte
21+
// nb. does *not* implement yaml.Marshaler
22+
func (d MarshaledWithHumanOutput) MarshalYAML() ([]byte, error) {
23+
// Marshal to JSON and convert to YAML. This is to use key names from json field tags.
24+
jsonBytes, err := OnlyMarshaled{Value: d.Value}.MarshalJSON()
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
return jsonToYaml(jsonBytes)
30+
}
31+
32+
// MarshalHuman implements output.Output
33+
// For MarshaledWithHumanDetails outputs, we return *only* the details part in humanized output
34+
func (d MarshaledWithHumanOutput) MarshalHuman() ([]byte, error) {
35+
return d.Output.MarshalHuman()
36+
}
37+
38+
// MarshalRawMap implements output.Output
39+
func (d MarshaledWithHumanOutput) MarshalRawMap() (map[string]interface{}, error) {
40+
return nil, errors.New("MarshaledWithHumanOutput output should not be used as part of multiple output, raw output is undefined")
41+
}
42+
43+
func jsonToYaml(jsonIn []byte) ([]byte, error) {
44+
var yamlObj interface{}
45+
if err := yaml.Unmarshal(jsonIn, &yamlObj); err != nil {
46+
return nil, err
47+
}
48+
return yaml.Marshal(yamlObj)
49+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package output_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/UpCloudLtd/upcloud-cli/internal/output"
7+
)
8+
9+
func TestMarshaledWithHumanOutput(t *testing.T) {
10+
var marshaledWithHumanOutputTests = []outputTestCase{
11+
{
12+
name: "struct",
13+
input: output.MarshaledWithHumanOutput{
14+
Value: struct {
15+
KeyFromTag string `json:"key_from_tag"`
16+
}{"mock"},
17+
Output: output.None{},
18+
},
19+
expectedHumanResult: "",
20+
expectedJSONResult: "{\n \"key_from_tag\": \"mock\"\n}",
21+
expectedYAMLResult: "key_from_tag: mock\n",
22+
},
23+
}
24+
for _, test := range marshaledWithHumanOutputTests {
25+
t.Run(test.name, test.Generate())
26+
}
27+
}

0 commit comments

Comments
 (0)