Skip to content

Commit aed825d

Browse files
authored
Adding an age column to the cli (hetznercloud#420)
* Adding an age column to the cli, closes hetznercloud#417 As an SRE, I deploy a lot of hcloud resources automatically via the hcloud API and use the cli to validate those changes. I am deploying using immutable VMs rather than phoenix deployments (instead of updating existing VMs, I re-create them with the newest config) Since I am using the API in automation to create hcloud resources, my hcloud resources include Ids in their name, making it a mess sometimes to figure out which is the newest VM. To know which VM got deployed when, I often find myself using the hcloud-cli like this: ```bash $ hcloud server list -s name -o 'columns=name,status,created,ipv4,ipv6' NAME STATUS CREATED IPV4 IPV6 cedi-dev-control-plane-xxxxx running Sun Nov 13 13:43:05 CET 2022 XXX.XXX.XXX.XXX 2a01:4f8:xxxx:xxxx::/64 cedi-dev-control-plane-xxxxx running Sun Nov 13 13:51:46 CET 2022 XXX.XXX.XXX.XXX 2a01:4f8:xxxx:xxxx::/64 cedi-dev-control-plane-xxxxx running Sun Nov 13 13:40:22 CET 2022 XXX.XXX.XXX.XXX 2a01:4f8:xxxx:xxxx::/64 cedi-dev-worker-cxp31-xxxxx running Wed Nov 9 17:15:32 CET 2022 XXX.XXX.XXX.XXX 2a01:4f8:xxxx:xxxx::/64 cedi-dev-worker-cxp31-xxxxx running Wed Nov 9 17:26:36 CET 2022 XXX.XXX.XXX.XXX 2a01:4f8:xxxx:xxxx::/64 cedi-dev-worker-cxp31-xxxxx running Wed Nov 9 16:46:55 CET 2022 XXX.XXX.XXX.XXX 2a01:4f8:xxxx:xxxx::/64 cedi-dev-worker-cxp31-xxxxx running Sun Nov 13 14:02:41 CET 2022 XXX.XXX.XXX.XXX 2a01:4f8:xxxx:xxxx::/64 ``` However, I noticed the `created` column contains the "raw" create DateTime string which is good for computers to read, but bad for humans. Using the hcloud dashboard in my browser, I can see the created timestamp as a Duration since `time.Now()`: <img width="215" alt="Screenshot 2022-11-13 at 14 17 18" src="https://user-images.githubusercontent.com/1952599/201523632-35a91d6d-4039-4469-a308-6c2355f70652.png"> With this commit I add a "age" column to the output of hcloud cli: ```bash $ hcloud server list ID NAME STATUS IPV4 IPV6 PRIVATE NET DATACENTER AGE 25550867 cedi-dev-control-plane-xxxxx running xxx.xxx.xx.xxx 2a01:4f8:xxxx:xxxx::/64 10.0.0.x (cedi-dev) fsn1-dcxx 2d 25551100 cedi-dev-control-plane-xxxxx running xx.xx.xxx.xx 2a01:4f8:xxx:xxxx::/64 10.0.0.x (cedi-dev) fsn1-dcxx 2d 25551348 cedi-dev-worker-cxp31-xxxxx running xx.xx.xx.xxx 2a01:4f8:xxxx:xxxx::/64 10.0.0.x (cedi-dev) fsn1-dcxx 2d 25586128 cedi-dev-worker-cxp31-xxxxx running xx.xx.xxx.xx 2a01:4f8:xxxx:xxxx::/64 10.0.0.x (cedi-dev) fsn1-dcxx 1d 25586289 cedi-dev-control-plane-xxxxx running xxx.xx.xx.xx 2a01:4f8:xxxx:xxxx::/64 10.0.0.x (cedi-dev) fsn1-dcxx 1d 25588261 cedi-dev-worker-cxp31-xxxxx running xxx.xx.xxx.xxx 2a01:4f8:xxx:xxxx::/64 10.0.0.x (cedi-dev) fsn1-dcxx 23h ``` I also added the "age" column to the "default_columns" in most commands
1 parent 59f0ab3 commit aed825d

File tree

15 files changed

+159
-16
lines changed

15 files changed

+159
-16
lines changed

internal/cmd/certificate/list.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package certificate
33
import (
44
"context"
55
"strings"
6+
"time"
67

78
"github.com/hetznercloud/cli/internal/cmd/base"
89
"github.com/hetznercloud/cli/internal/hcapi2"
@@ -16,7 +17,7 @@ import (
1617

1718
var listCmd = base.ListCmd{
1819
ResourceNamePlural: "certificates",
19-
DefaultColumns: []string{"id", "name", "type", "domain_names", "not_valid_after"},
20+
DefaultColumns: []string{"id", "name", "type", "domain_names", "not_valid_after", "age"},
2021

2122
Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
2223
opts := hcloud.CertificateListOpts{ListOpts: listOpts}
@@ -70,6 +71,10 @@ var listCmd = base.ListCmd{
7071
AddFieldFn("created", output.FieldFn(func(obj interface{}) string {
7172
cert := obj.(*hcloud.Certificate)
7273
return util.Datetime(cert.Created)
74+
})).
75+
AddFieldFn("age", output.FieldFn(func(obj interface{}) string {
76+
cert := obj.(*hcloud.Certificate)
77+
return util.Age(cert.Created, time.Now())
7378
}))
7479
},
7580

internal/cmd/floatingip/list.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"strings"
7+
"time"
78

89
"github.com/spf13/cobra"
910

@@ -19,7 +20,7 @@ import (
1920

2021
var listCmd = base.ListCmd{
2122
ResourceNamePlural: "Floating IPs",
22-
DefaultColumns: []string{"id", "type", "name", "description", "ip", "home", "server", "dns"},
23+
DefaultColumns: []string{"id", "type", "name", "description", "ip", "home", "server", "dns", "age"},
2324

2425
Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
2526
opts := hcloud.FloatingIPListOpts{ListOpts: listOpts}
@@ -85,6 +86,10 @@ var listCmd = base.ListCmd{
8586
AddFieldFn("created", output.FieldFn(func(obj interface{}) string {
8687
floatingIP := obj.(*hcloud.FloatingIP)
8788
return util.Datetime(floatingIP.Created)
89+
})).
90+
AddFieldFn("age", output.FieldFn(func(obj interface{}) string {
91+
floatingIP := obj.(*hcloud.FloatingIP)
92+
return util.Age(floatingIP.Created, time.Now())
8893
}))
8994
},
9095

internal/cmd/image/list.go

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"strings"
7+
"time"
78

89
"github.com/hetznercloud/cli/internal/cmd/base"
910
"github.com/hetznercloud/cli/internal/cmd/cmpl"
@@ -102,6 +103,10 @@ var listCmd = base.ListCmd{
102103
image := obj.(*hcloud.Image)
103104
return util.Datetime(image.Created)
104105
})).
106+
AddFieldFn("age", output.FieldFn(func(obj interface{}) string {
107+
image := obj.(*hcloud.Image)
108+
return util.Age(image.Created, time.Now())
109+
})).
105110
AddFieldFn("deprecated", output.FieldFn(func(obj interface{}) string {
106111
image := obj.(*hcloud.Image)
107112
if image.Deprecated.IsZero() {

internal/cmd/loadbalancer/list.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package loadbalancer
33
import (
44
"context"
55
"strings"
6+
"time"
67

78
"github.com/hetznercloud/cli/internal/cmd/base"
89
"github.com/hetznercloud/cli/internal/cmd/output"
@@ -17,7 +18,7 @@ import (
1718
var ListCmd = base.ListCmd{
1819
ResourceNamePlural: "Load Balancer",
1920

20-
DefaultColumns: []string{"id", "name", "ipv4", "ipv6", "type", "location", "network_zone"},
21+
DefaultColumns: []string{"id", "name", "ipv4", "ipv6", "type", "location", "network_zone", "age"},
2122
Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
2223
opts := hcloud.LoadBalancerListOpts{ListOpts: listOpts}
2324
if len(sorts) > 0 {
@@ -70,6 +71,10 @@ var ListCmd = base.ListCmd{
7071
AddFieldFn("created", output.FieldFn(func(obj interface{}) string {
7172
loadBalancer := obj.(*hcloud.LoadBalancer)
7273
return util.Datetime(loadBalancer.Created)
74+
})).
75+
AddFieldFn("age", output.FieldFn(func(obj interface{}) string {
76+
loadBalancer := obj.(*hcloud.LoadBalancer)
77+
return util.Age(loadBalancer.Created, time.Now())
7378
}))
7479
},
7580

internal/cmd/network/list.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"strings"
7+
"time"
78

89
"github.com/hetznercloud/cli/internal/cmd/base"
910
"github.com/hetznercloud/cli/internal/cmd/output"
@@ -16,7 +17,7 @@ import (
1617

1718
var ListCmd = base.ListCmd{
1819
ResourceNamePlural: "networks",
19-
DefaultColumns: []string{"id", "name", "ip_range", "servers"},
20+
DefaultColumns: []string{"id", "name", "ip_range", "servers", "age"},
2021

2122
Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
2223
opts := hcloud.NetworkListOpts{ListOpts: listOpts}
@@ -62,6 +63,10 @@ var ListCmd = base.ListCmd{
6263
AddFieldFn("created", output.FieldFn(func(obj interface{}) string {
6364
network := obj.(*hcloud.Network)
6465
return util.Datetime(network.Created)
66+
})).
67+
AddFieldFn("age", output.FieldFn(func(obj interface{}) string {
68+
network := obj.(*hcloud.Network)
69+
return util.Age(network.Created, time.Now())
6570
}))
6671
},
6772

internal/cmd/network/list_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"net"
66
"testing"
7+
"time"
78

89
"github.com/golang/mock/gomock"
910
"github.com/hetznercloud/cli/internal/cmd/network"
@@ -36,14 +37,15 @@ func TestList(t *testing.T) {
3637
Name: "test-net",
3738
IPRange: &net.IPNet{IP: net.ParseIP("192.0.2.1"), Mask: net.CIDRMask(24, 32)},
3839
Servers: []*hcloud.Server{{ID: 3421}},
40+
Created: time.Now().Add(-10 * time.Second),
3941
},
4042
},
4143
nil)
4244

4345
out, err := fx.Run(cmd, []string{"--selector", "foo=bar"})
4446

45-
expOut := `ID NAME IP RANGE SERVERS
46-
123 test-net 192.0.2.1/24 1 server
47+
expOut := `ID NAME IP RANGE SERVERS AGE
48+
123 test-net 192.0.2.1/24 1 server 10s
4749
`
4850

4951
assert.NoError(t, err)

internal/cmd/placementgroup/list.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package placementgroup
33
import (
44
"context"
55
"fmt"
6+
"time"
67

78
"github.com/hetznercloud/cli/internal/cmd/base"
89
"github.com/hetznercloud/cli/internal/cmd/output"
10+
"github.com/hetznercloud/cli/internal/cmd/util"
911
"github.com/hetznercloud/cli/internal/hcapi2"
1012
"github.com/hetznercloud/hcloud-go/hcloud"
1113
"github.com/hetznercloud/hcloud-go/hcloud/schema"
@@ -14,7 +16,7 @@ import (
1416

1517
var ListCmd = base.ListCmd{
1618
ResourceNamePlural: "placement groups",
17-
DefaultColumns: []string{"id", "name", "servers", "type"},
19+
DefaultColumns: []string{"id", "name", "servers", "type", "age"},
1820

1921
Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
2022
opts := hcloud.PlacementGroupListOpts{ListOpts: listOpts}
@@ -40,6 +42,14 @@ var ListCmd = base.ListCmd{
4042
return fmt.Sprintf("%d server", count)
4143
}
4244
return fmt.Sprintf("%d servers", count)
45+
})).
46+
AddFieldFn("created", output.FieldFn(func(obj interface{}) string {
47+
placementGroup := obj.(*hcloud.PlacementGroup)
48+
return util.Datetime(placementGroup.Created)
49+
})).
50+
AddFieldFn("age", output.FieldFn(func(obj interface{}) string {
51+
placementGroup := obj.(*hcloud.PlacementGroup)
52+
return util.Age(placementGroup.Created, time.Now())
4353
}))
4454
},
4555

internal/cmd/placementgroup/list_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package placementgroup_test
33
import (
44
"context"
55
"testing"
6+
"time"
67

78
"github.com/golang/mock/gomock"
89
"github.com/hetznercloud/cli/internal/cmd/placementgroup"
@@ -39,13 +40,14 @@ func TestList(t *testing.T) {
3940
Labels: map[string]string{"key": "value"},
4041
Servers: []int{4711, 4712},
4142
Type: hcloud.PlacementGroupTypeSpread,
43+
Created: time.Now().Add(-10 * time.Second),
4244
},
4345
}, nil)
4446

4547
out, err := fx.Run(cmd, []string{"--selector", "foo=bar"})
4648

47-
expOut := `ID NAME SERVERS TYPE
48-
897 my Placement Group 2 servers spread
49+
expOut := `ID NAME SERVERS TYPE AGE
50+
897 my Placement Group 2 servers spread 10s
4951
`
5052

5153
assert.NoError(t, err)

internal/cmd/primaryip/list.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"strings"
7+
"time"
78

89
"github.com/hetznercloud/hcloud-go/hcloud/schema"
910

@@ -17,7 +18,7 @@ import (
1718

1819
var listCmd = base.ListCmd{
1920
ResourceNamePlural: "Primary IPs",
20-
DefaultColumns: []string{"id", "type", "name", "ip", "assignee", "dns", "auto_delete"},
21+
DefaultColumns: []string{"id", "type", "name", "ip", "assignee", "dns", "auto_delete", "age"},
2122

2223
Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
2324
opts := hcloud.PrimaryIPListOpts{ListOpts: listOpts}
@@ -79,6 +80,10 @@ var listCmd = base.ListCmd{
7980
AddFieldFn("created", output.FieldFn(func(obj interface{}) string {
8081
primaryIP := obj.(*hcloud.PrimaryIP)
8182
return util.Datetime(primaryIP.Created)
83+
})).
84+
AddFieldFn("age", output.FieldFn(func(obj interface{}) string {
85+
primaryIP := obj.(*hcloud.PrimaryIP)
86+
return util.Age(primaryIP.Created, time.Now())
8287
}))
8388
},
8489

internal/cmd/primaryip/list_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"net"
66
"testing"
7+
"time"
78

89
"github.com/golang/mock/gomock"
910
"github.com/hetznercloud/cli/internal/testutil"
@@ -36,14 +37,15 @@ func TestList(t *testing.T) {
3637
AutoDelete: true,
3738
Type: hcloud.PrimaryIPTypeIPv4,
3839
IP: net.ParseIP("127.0.0.1"),
40+
Created: time.Now().Add(-10 * time.Second),
3941
},
4042
},
4143
nil)
4244

4345
out, err := fx.Run(cmd, []string{"--selector", "foo=bar"})
4446

45-
expOut := `ID TYPE NAME IP ASSIGNEE DNS AUTO DELETE
46-
123 ipv4 test-net 127.0.0.1 - - yes
47+
expOut := `ID TYPE NAME IP ASSIGNEE DNS AUTO DELETE AGE
48+
123 ipv4 test-net 127.0.0.1 - - yes 10s
4749
`
4850

4951
assert.NoError(t, err)

internal/cmd/server/list.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"strconv"
77
"strings"
8+
"time"
89

910
humanize "github.com/dustin/go-humanize"
1011
"github.com/hetznercloud/cli/internal/cmd/base"
@@ -20,7 +21,7 @@ import (
2021
var ListCmd = base.ListCmd{
2122
ResourceNamePlural: "servers",
2223

23-
DefaultColumns: []string{"id", "name", "status", "ipv4", "ipv6", "private_net", "datacenter"},
24+
DefaultColumns: []string{"id", "name", "status", "ipv4", "ipv6", "private_net", "datacenter", "age"},
2425

2526
Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
2627
opts := hcloud.ServerListOpts{ListOpts: listOpts}
@@ -113,6 +114,10 @@ var ListCmd = base.ListCmd{
113114
server := obj.(*hcloud.Server)
114115
return util.Datetime(server.Created)
115116
})).
117+
AddFieldFn("age", output.FieldFn(func(obj interface{}) string {
118+
server := obj.(*hcloud.Server)
119+
return util.Age(server.Created, time.Now())
120+
})).
116121
AddFieldFn("placement_group", output.FieldFn(func(obj interface{}) string {
117122
server := obj.(*hcloud.Server)
118123
if server.PlacementGroup == nil {

internal/cmd/sshkey/list.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package sshkey
22

33
import (
44
"context"
5+
"time"
56

67
"github.com/hetznercloud/cli/internal/cmd/base"
78
"github.com/hetznercloud/cli/internal/cmd/output"
@@ -14,7 +15,7 @@ import (
1415

1516
var listCmd = base.ListCmd{
1617
ResourceNamePlural: "ssh keys",
17-
DefaultColumns: []string{"id", "name", "fingerprint"},
18+
DefaultColumns: []string{"id", "name", "fingerprint", "age"},
1819

1920
Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
2021
opts := hcloud.SSHKeyListOpts{ListOpts: listOpts}
@@ -40,6 +41,10 @@ var listCmd = base.ListCmd{
4041
AddFieldFn("created", output.FieldFn(func(obj interface{}) string {
4142
sshKey := obj.(*hcloud.SSHKey)
4243
return util.Datetime(sshKey.Created)
44+
})).
45+
AddFieldFn("age", output.FieldFn(func(obj interface{}) string {
46+
sshKey := obj.(*hcloud.SSHKey)
47+
return util.Age(sshKey.Created, time.Now())
4348
}))
4449
},
4550

internal/cmd/util/util.go

+23
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,29 @@ func Datetime(t time.Time) string {
3434
return t.Local().Format(time.UnixDate)
3535
}
3636

37+
func Age(t, currentTime time.Time) string {
38+
diff := currentTime.Sub(t)
39+
40+
if int(diff.Hours()) >= 24 {
41+
days := int(diff.Hours()) / 24
42+
return fmt.Sprintf("%dd", days)
43+
}
44+
45+
if int(diff.Hours()) > 0 {
46+
return fmt.Sprintf("%dh", int(diff.Hours()))
47+
}
48+
49+
if int(diff.Minutes()) > 0 {
50+
return fmt.Sprintf("%dm", int(diff.Minutes()))
51+
}
52+
53+
if int(diff.Seconds()) > 0 {
54+
return fmt.Sprintf("%ds", int(diff.Seconds()))
55+
}
56+
57+
return "just now"
58+
}
59+
3760
func ChainRunE(fns ...func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error {
3861
return func(cmd *cobra.Command, args []string) error {
3962
for _, fn := range fns {

0 commit comments

Comments
 (0)