From f2e6d7c5f9eed17bdebfdd396ce6587ea2c0161d Mon Sep 17 00:00:00 2001 From: Cedric Kienzler Date: Tue, 15 Nov 2022 16:12:22 +0100 Subject: [PATCH 1/4] Adding an age column to the cli, closes #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()`: Screenshot 2022-11-13 at 14 17 18 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 --- internal/cmd/certificate/list.go | 6 +++++- internal/cmd/floatingip/list.go | 6 +++++- internal/cmd/image/list.go | 4 ++++ internal/cmd/loadbalancer/list.go | 6 +++++- internal/cmd/network/list.go | 6 +++++- internal/cmd/placementgroup/list.go | 11 ++++++++++- internal/cmd/primaryip/list.go | 6 +++++- internal/cmd/server/list.go | 6 +++++- internal/cmd/sshkey/list.go | 6 +++++- internal/cmd/util/util.go | 24 ++++++++++++++++++++++++ internal/cmd/volume/list.go | 6 +++++- 11 files changed, 78 insertions(+), 9 deletions(-) diff --git a/internal/cmd/certificate/list.go b/internal/cmd/certificate/list.go index 1133f03c..9bd476be 100644 --- a/internal/cmd/certificate/list.go +++ b/internal/cmd/certificate/list.go @@ -16,7 +16,7 @@ import ( var listCmd = base.ListCmd{ ResourceNamePlural: "certificates", - DefaultColumns: []string{"id", "name", "type", "domain_names", "not_valid_after"}, + DefaultColumns: []string{"id", "name", "type", "domain_names", "not_valid_after", "age"}, Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) { opts := hcloud.CertificateListOpts{ListOpts: listOpts} @@ -70,6 +70,10 @@ var listCmd = base.ListCmd{ AddFieldFn("created", output.FieldFn(func(obj interface{}) string { cert := obj.(*hcloud.Certificate) return util.Datetime(cert.Created) + })). + AddFieldFn("age", output.FieldFn(func(obj interface{}) string { + cert := obj.(*hcloud.Certificate) + return util.Age(cert.Created) })) }, diff --git a/internal/cmd/floatingip/list.go b/internal/cmd/floatingip/list.go index 4dbb49ab..098bac49 100644 --- a/internal/cmd/floatingip/list.go +++ b/internal/cmd/floatingip/list.go @@ -19,7 +19,7 @@ import ( var listCmd = base.ListCmd{ ResourceNamePlural: "Floating IPs", - DefaultColumns: []string{"id", "type", "name", "description", "ip", "home", "server", "dns"}, + DefaultColumns: []string{"id", "type", "name", "description", "ip", "home", "server", "dns", "age"}, Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) { opts := hcloud.FloatingIPListOpts{ListOpts: listOpts} @@ -85,6 +85,10 @@ var listCmd = base.ListCmd{ AddFieldFn("created", output.FieldFn(func(obj interface{}) string { floatingIP := obj.(*hcloud.FloatingIP) return util.Datetime(floatingIP.Created) + })). + AddFieldFn("age", output.FieldFn(func(obj interface{}) string { + floatingIP := obj.(*hcloud.FloatingIP) + return util.Age(floatingIP.Created) })) }, diff --git a/internal/cmd/image/list.go b/internal/cmd/image/list.go index 50b71b58..0b71631a 100644 --- a/internal/cmd/image/list.go +++ b/internal/cmd/image/list.go @@ -102,6 +102,10 @@ var listCmd = base.ListCmd{ image := obj.(*hcloud.Image) return util.Datetime(image.Created) })). + AddFieldFn("age", output.FieldFn(func(obj interface{}) string { + image := obj.(*hcloud.Image) + return util.Age(image.Created) + })). AddFieldFn("deprecated", output.FieldFn(func(obj interface{}) string { image := obj.(*hcloud.Image) if image.Deprecated.IsZero() { diff --git a/internal/cmd/loadbalancer/list.go b/internal/cmd/loadbalancer/list.go index 46520591..8b4dfecb 100644 --- a/internal/cmd/loadbalancer/list.go +++ b/internal/cmd/loadbalancer/list.go @@ -17,7 +17,7 @@ import ( var ListCmd = base.ListCmd{ ResourceNamePlural: "Load Balancer", - DefaultColumns: []string{"id", "name", "ipv4", "ipv6", "type", "location", "network_zone"}, + DefaultColumns: []string{"id", "name", "ipv4", "ipv6", "type", "location", "network_zone", "age"}, Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) { opts := hcloud.LoadBalancerListOpts{ListOpts: listOpts} if len(sorts) > 0 { @@ -70,6 +70,10 @@ var ListCmd = base.ListCmd{ AddFieldFn("created", output.FieldFn(func(obj interface{}) string { loadBalancer := obj.(*hcloud.LoadBalancer) return util.Datetime(loadBalancer.Created) + })). + AddFieldFn("age", output.FieldFn(func(obj interface{}) string { + loadBalancer := obj.(*hcloud.LoadBalancer) + return util.Age(loadBalancer.Created) })) }, diff --git a/internal/cmd/network/list.go b/internal/cmd/network/list.go index b50d5e9f..a4c98136 100644 --- a/internal/cmd/network/list.go +++ b/internal/cmd/network/list.go @@ -16,7 +16,7 @@ import ( var ListCmd = base.ListCmd{ ResourceNamePlural: "networks", - DefaultColumns: []string{"id", "name", "ip_range", "servers"}, + DefaultColumns: []string{"id", "name", "ip_range", "servers", "age"}, Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) { opts := hcloud.NetworkListOpts{ListOpts: listOpts} @@ -62,6 +62,10 @@ var ListCmd = base.ListCmd{ AddFieldFn("created", output.FieldFn(func(obj interface{}) string { network := obj.(*hcloud.Network) return util.Datetime(network.Created) + })). + AddFieldFn("age", output.FieldFn(func(obj interface{}) string { + network := obj.(*hcloud.Network) + return util.Age(network.Created) })) }, diff --git a/internal/cmd/placementgroup/list.go b/internal/cmd/placementgroup/list.go index 67f15065..e31b91f5 100644 --- a/internal/cmd/placementgroup/list.go +++ b/internal/cmd/placementgroup/list.go @@ -6,6 +6,7 @@ import ( "github.com/hetznercloud/cli/internal/cmd/base" "github.com/hetznercloud/cli/internal/cmd/output" + "github.com/hetznercloud/cli/internal/cmd/util" "github.com/hetznercloud/cli/internal/hcapi2" "github.com/hetznercloud/hcloud-go/hcloud" "github.com/hetznercloud/hcloud-go/hcloud/schema" @@ -14,7 +15,7 @@ import ( var ListCmd = base.ListCmd{ ResourceNamePlural: "placement groups", - DefaultColumns: []string{"id", "name", "servers", "type"}, + DefaultColumns: []string{"id", "name", "servers", "type", "age"}, Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) { opts := hcloud.PlacementGroupListOpts{ListOpts: listOpts} @@ -40,6 +41,14 @@ var ListCmd = base.ListCmd{ return fmt.Sprintf("%d server", count) } return fmt.Sprintf("%d servers", count) + })). + AddFieldFn("created", output.FieldFn(func(obj interface{}) string { + placementGroup := obj.(*hcloud.PlacementGroup) + return util.Datetime(placementGroup.Created) + })). + AddFieldFn("age", output.FieldFn(func(obj interface{}) string { + placementGroup := obj.(*hcloud.PlacementGroup) + return util.Age(placementGroup.Created) })) }, diff --git a/internal/cmd/primaryip/list.go b/internal/cmd/primaryip/list.go index 982e8dd7..cea226d1 100644 --- a/internal/cmd/primaryip/list.go +++ b/internal/cmd/primaryip/list.go @@ -17,7 +17,7 @@ import ( var listCmd = base.ListCmd{ ResourceNamePlural: "Primary IPs", - DefaultColumns: []string{"id", "type", "name", "ip", "assignee", "dns", "auto_delete"}, + DefaultColumns: []string{"id", "type", "name", "ip", "assignee", "dns", "auto_delete", "age"}, Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) { opts := hcloud.PrimaryIPListOpts{ListOpts: listOpts} @@ -79,6 +79,10 @@ var listCmd = base.ListCmd{ AddFieldFn("created", output.FieldFn(func(obj interface{}) string { primaryIP := obj.(*hcloud.PrimaryIP) return util.Datetime(primaryIP.Created) + })). + AddFieldFn("age", output.FieldFn(func(obj interface{}) string { + primaryIP := obj.(*hcloud.PrimaryIP) + return util.Age(primaryIP.Created) })) }, diff --git a/internal/cmd/server/list.go b/internal/cmd/server/list.go index 3d91e4cd..5622ca97 100644 --- a/internal/cmd/server/list.go +++ b/internal/cmd/server/list.go @@ -20,7 +20,7 @@ import ( var ListCmd = base.ListCmd{ ResourceNamePlural: "servers", - DefaultColumns: []string{"id", "name", "status", "ipv4", "ipv6", "private_net", "datacenter"}, + DefaultColumns: []string{"id", "name", "status", "ipv4", "ipv6", "private_net", "datacenter", "age"}, Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) { opts := hcloud.ServerListOpts{ListOpts: listOpts} @@ -113,6 +113,10 @@ var ListCmd = base.ListCmd{ server := obj.(*hcloud.Server) return util.Datetime(server.Created) })). + AddFieldFn("age", output.FieldFn(func(obj interface{}) string { + server := obj.(*hcloud.Server) + return util.Age(server.Created) + })). AddFieldFn("placement_group", output.FieldFn(func(obj interface{}) string { server := obj.(*hcloud.Server) if server.PlacementGroup == nil { diff --git a/internal/cmd/sshkey/list.go b/internal/cmd/sshkey/list.go index 26d7d205..ceecd098 100644 --- a/internal/cmd/sshkey/list.go +++ b/internal/cmd/sshkey/list.go @@ -14,7 +14,7 @@ import ( var listCmd = base.ListCmd{ ResourceNamePlural: "ssh keys", - DefaultColumns: []string{"id", "name", "fingerprint"}, + DefaultColumns: []string{"id", "name", "fingerprint", "age"}, Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) { opts := hcloud.SSHKeyListOpts{ListOpts: listOpts} @@ -40,6 +40,10 @@ var listCmd = base.ListCmd{ AddFieldFn("created", output.FieldFn(func(obj interface{}) string { sshKey := obj.(*hcloud.SSHKey) return util.Datetime(sshKey.Created) + })). + AddFieldFn("age", output.FieldFn(func(obj interface{}) string { + sshKey := obj.(*hcloud.SSHKey) + return util.Age(sshKey.Created) })) }, diff --git a/internal/cmd/util/util.go b/internal/cmd/util/util.go index c9e45874..5f69314b 100644 --- a/internal/cmd/util/util.go +++ b/internal/cmd/util/util.go @@ -34,6 +34,30 @@ func Datetime(t time.Time) string { return t.Local().Format(time.UnixDate) } +func Age(t time.Time) string { + currentTime := time.Now() + diff := currentTime.Sub(t) + + if diff.Hours() >= 24 { + days := int(diff.Hours()) / 24 + return fmt.Sprintf("%dd", days) + } + + if diff.Hours() > 0 { + return fmt.Sprintf("%dh", int(diff.Hours())) + } + + if diff.Minutes() > 0 { + return fmt.Sprintf("%dm", int(diff.Minutes())) + } + + if diff.Seconds() > 0 { + return fmt.Sprintf("%ds", int(diff.Seconds())) + } + + return "just now" +} + func ChainRunE(fns ...func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { for _, fn := range fns { diff --git a/internal/cmd/volume/list.go b/internal/cmd/volume/list.go index 4a370df2..c9ec11ce 100644 --- a/internal/cmd/volume/list.go +++ b/internal/cmd/volume/list.go @@ -19,7 +19,7 @@ import ( var listCmd = base.ListCmd{ ResourceNamePlural: "volumes", - DefaultColumns: []string{"id", "name", "size", "server", "location"}, + DefaultColumns: []string{"id", "name", "size", "server", "location", "age"}, Fetch: func(ctx context.Context, client hcapi2.Client, cmd *cobra.Command, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) { opts := hcloud.VolumeListOpts{ListOpts: listOpts} @@ -69,6 +69,10 @@ var listCmd = base.ListCmd{ AddFieldFn("created", output.FieldFn(func(obj interface{}) string { volume := obj.(*hcloud.Volume) return util.Datetime(volume.Created) + })). + AddFieldFn("age", output.FieldFn(func(obj interface{}) string { + volume := obj.(*hcloud.Volume) + return util.Age(volume.Created) })) }, From 78ecb37fb57b5080ba2581fe7c4c95f5ee088486 Mon Sep 17 00:00:00 2001 From: Cedric Kienzler Date: Thu, 17 Nov 2022 15:39:52 +0100 Subject: [PATCH 2/4] Add unit-testing This commit adds unit-testing to the `Ago` function as well as fixing existing unit-tests --- internal/cmd/certificate/list.go | 3 +- internal/cmd/floatingip/list.go | 3 +- internal/cmd/image/list.go | 3 +- internal/cmd/loadbalancer/list.go | 3 +- internal/cmd/network/list.go | 3 +- internal/cmd/network/list_test.go | 4 +- internal/cmd/placementgroup/list.go | 3 +- internal/cmd/placementgroup/list_test.go | 4 +- internal/cmd/primaryip/list.go | 3 +- internal/cmd/primaryip/list_test.go | 4 +- internal/cmd/server/list.go | 3 +- internal/cmd/sshkey/list.go | 3 +- internal/cmd/util/util.go | 11 ++--- internal/cmd/util/util_internal_test.go | 61 +++++++++++++++++++++++- internal/cmd/volume/list.go | 3 +- 15 files changed, 91 insertions(+), 23 deletions(-) diff --git a/internal/cmd/certificate/list.go b/internal/cmd/certificate/list.go index 9bd476be..9a8468d5 100644 --- a/internal/cmd/certificate/list.go +++ b/internal/cmd/certificate/list.go @@ -3,6 +3,7 @@ package certificate import ( "context" "strings" + "time" "github.com/hetznercloud/cli/internal/cmd/base" "github.com/hetznercloud/cli/internal/hcapi2" @@ -73,7 +74,7 @@ var listCmd = base.ListCmd{ })). AddFieldFn("age", output.FieldFn(func(obj interface{}) string { cert := obj.(*hcloud.Certificate) - return util.Age(cert.Created) + return util.Age(cert.Created, time.Now()) })) }, diff --git a/internal/cmd/floatingip/list.go b/internal/cmd/floatingip/list.go index 098bac49..500050a7 100644 --- a/internal/cmd/floatingip/list.go +++ b/internal/cmd/floatingip/list.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/spf13/cobra" @@ -88,7 +89,7 @@ var listCmd = base.ListCmd{ })). AddFieldFn("age", output.FieldFn(func(obj interface{}) string { floatingIP := obj.(*hcloud.FloatingIP) - return util.Age(floatingIP.Created) + return util.Age(floatingIP.Created, time.Now()) })) }, diff --git a/internal/cmd/image/list.go b/internal/cmd/image/list.go index 0b71631a..282375db 100644 --- a/internal/cmd/image/list.go +++ b/internal/cmd/image/list.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/hetznercloud/cli/internal/cmd/base" "github.com/hetznercloud/cli/internal/cmd/cmpl" @@ -104,7 +105,7 @@ var listCmd = base.ListCmd{ })). AddFieldFn("age", output.FieldFn(func(obj interface{}) string { image := obj.(*hcloud.Image) - return util.Age(image.Created) + return util.Age(image.Created, time.Now()) })). AddFieldFn("deprecated", output.FieldFn(func(obj interface{}) string { image := obj.(*hcloud.Image) diff --git a/internal/cmd/loadbalancer/list.go b/internal/cmd/loadbalancer/list.go index 8b4dfecb..1e0eca45 100644 --- a/internal/cmd/loadbalancer/list.go +++ b/internal/cmd/loadbalancer/list.go @@ -3,6 +3,7 @@ package loadbalancer import ( "context" "strings" + "time" "github.com/hetznercloud/cli/internal/cmd/base" "github.com/hetznercloud/cli/internal/cmd/output" @@ -73,7 +74,7 @@ var ListCmd = base.ListCmd{ })). AddFieldFn("age", output.FieldFn(func(obj interface{}) string { loadBalancer := obj.(*hcloud.LoadBalancer) - return util.Age(loadBalancer.Created) + return util.Age(loadBalancer.Created, time.Now()) })) }, diff --git a/internal/cmd/network/list.go b/internal/cmd/network/list.go index a4c98136..7435a82d 100644 --- a/internal/cmd/network/list.go +++ b/internal/cmd/network/list.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/hetznercloud/cli/internal/cmd/base" "github.com/hetznercloud/cli/internal/cmd/output" @@ -65,7 +66,7 @@ var ListCmd = base.ListCmd{ })). AddFieldFn("age", output.FieldFn(func(obj interface{}) string { network := obj.(*hcloud.Network) - return util.Age(network.Created) + return util.Age(network.Created, time.Now()) })) }, diff --git a/internal/cmd/network/list_test.go b/internal/cmd/network/list_test.go index aee988a7..c928ad6a 100644 --- a/internal/cmd/network/list_test.go +++ b/internal/cmd/network/list_test.go @@ -42,8 +42,8 @@ func TestList(t *testing.T) { out, err := fx.Run(cmd, []string{"--selector", "foo=bar"}) - expOut := `ID NAME IP RANGE SERVERS -123 test-net 192.0.2.1/24 1 server + expOut := `ID NAME IP RANGE SERVERS AGE +123 test-net 192.0.2.1/24 1 server 106751d ` assert.NoError(t, err) diff --git a/internal/cmd/placementgroup/list.go b/internal/cmd/placementgroup/list.go index e31b91f5..39f746b1 100644 --- a/internal/cmd/placementgroup/list.go +++ b/internal/cmd/placementgroup/list.go @@ -3,6 +3,7 @@ package placementgroup import ( "context" "fmt" + "time" "github.com/hetznercloud/cli/internal/cmd/base" "github.com/hetznercloud/cli/internal/cmd/output" @@ -48,7 +49,7 @@ var ListCmd = base.ListCmd{ })). AddFieldFn("age", output.FieldFn(func(obj interface{}) string { placementGroup := obj.(*hcloud.PlacementGroup) - return util.Age(placementGroup.Created) + return util.Age(placementGroup.Created, time.Now()) })) }, diff --git a/internal/cmd/placementgroup/list_test.go b/internal/cmd/placementgroup/list_test.go index df07771c..955316da 100644 --- a/internal/cmd/placementgroup/list_test.go +++ b/internal/cmd/placementgroup/list_test.go @@ -44,8 +44,8 @@ func TestList(t *testing.T) { out, err := fx.Run(cmd, []string{"--selector", "foo=bar"}) - expOut := `ID NAME SERVERS TYPE -897 my Placement Group 2 servers spread + expOut := `ID NAME SERVERS TYPE AGE +897 my Placement Group 2 servers spread 106751d ` assert.NoError(t, err) diff --git a/internal/cmd/primaryip/list.go b/internal/cmd/primaryip/list.go index cea226d1..9526e256 100644 --- a/internal/cmd/primaryip/list.go +++ b/internal/cmd/primaryip/list.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" @@ -82,7 +83,7 @@ var listCmd = base.ListCmd{ })). AddFieldFn("age", output.FieldFn(func(obj interface{}) string { primaryIP := obj.(*hcloud.PrimaryIP) - return util.Age(primaryIP.Created) + return util.Age(primaryIP.Created, time.Now()) })) }, diff --git a/internal/cmd/primaryip/list_test.go b/internal/cmd/primaryip/list_test.go index f959c123..89f69dcb 100644 --- a/internal/cmd/primaryip/list_test.go +++ b/internal/cmd/primaryip/list_test.go @@ -42,8 +42,8 @@ func TestList(t *testing.T) { out, err := fx.Run(cmd, []string{"--selector", "foo=bar"}) - expOut := `ID TYPE NAME IP ASSIGNEE DNS AUTO DELETE -123 ipv4 test-net 127.0.0.1 - - yes + expOut := `ID TYPE NAME IP ASSIGNEE DNS AUTO DELETE AGE +123 ipv4 test-net 127.0.0.1 - - yes 106751d ` assert.NoError(t, err) diff --git a/internal/cmd/server/list.go b/internal/cmd/server/list.go index 5622ca97..77e67867 100644 --- a/internal/cmd/server/list.go +++ b/internal/cmd/server/list.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" "strings" + "time" humanize "github.com/dustin/go-humanize" "github.com/hetznercloud/cli/internal/cmd/base" @@ -115,7 +116,7 @@ var ListCmd = base.ListCmd{ })). AddFieldFn("age", output.FieldFn(func(obj interface{}) string { server := obj.(*hcloud.Server) - return util.Age(server.Created) + return util.Age(server.Created, time.Now()) })). AddFieldFn("placement_group", output.FieldFn(func(obj interface{}) string { server := obj.(*hcloud.Server) diff --git a/internal/cmd/sshkey/list.go b/internal/cmd/sshkey/list.go index ceecd098..58689b82 100644 --- a/internal/cmd/sshkey/list.go +++ b/internal/cmd/sshkey/list.go @@ -2,6 +2,7 @@ package sshkey import ( "context" + "time" "github.com/hetznercloud/cli/internal/cmd/base" "github.com/hetznercloud/cli/internal/cmd/output" @@ -43,7 +44,7 @@ var listCmd = base.ListCmd{ })). AddFieldFn("age", output.FieldFn(func(obj interface{}) string { sshKey := obj.(*hcloud.SSHKey) - return util.Age(sshKey.Created) + return util.Age(sshKey.Created, time.Now()) })) }, diff --git a/internal/cmd/util/util.go b/internal/cmd/util/util.go index 5f69314b..14c69506 100644 --- a/internal/cmd/util/util.go +++ b/internal/cmd/util/util.go @@ -34,24 +34,23 @@ func Datetime(t time.Time) string { return t.Local().Format(time.UnixDate) } -func Age(t time.Time) string { - currentTime := time.Now() +func Age(t, currentTime time.Time) string { diff := currentTime.Sub(t) - if diff.Hours() >= 24 { + if int(diff.Hours()) >= 24 { days := int(diff.Hours()) / 24 return fmt.Sprintf("%dd", days) } - if diff.Hours() > 0 { + if int(diff.Hours()) > 0 { return fmt.Sprintf("%dh", int(diff.Hours())) } - if diff.Minutes() > 0 { + if int(diff.Minutes()) > 0 { return fmt.Sprintf("%dm", int(diff.Minutes())) } - if diff.Seconds() > 0 { + if int(diff.Seconds()) > 0 { return fmt.Sprintf("%ds", int(diff.Seconds())) } diff --git a/internal/cmd/util/util_internal_test.go b/internal/cmd/util/util_internal_test.go index 3f5aa38c..2f8b7fb2 100644 --- a/internal/cmd/util/util_internal_test.go +++ b/internal/cmd/util/util_internal_test.go @@ -1,6 +1,9 @@ package util -import "testing" +import ( + "testing" + "time" +) func TestOnlyOneSet(t *testing.T) { tests := []struct { @@ -46,3 +49,59 @@ func TestOnlyOneSet(t *testing.T) { }) } } + +func TestAgo(t *testing.T) { + tests := []struct { + name string + t time.Time + now time.Time + expected string + }{ + { + name: "exactly now", + t: time.Date(2022, 11, 17, 15, 22, 12, 11, time.UTC), + now: time.Date(2022, 11, 17, 15, 22, 12, 11, time.UTC), + expected: "just now", + }, + { + name: "within a few milliseconds", + t: time.Date(2022, 11, 17, 15, 22, 12, 11, time.UTC), + now: time.Date(2022, 11, 17, 15, 22, 12, 21, time.UTC), + expected: "just now", + }, + { + name: "10 seconds", + t: time.Date(2022, 11, 17, 15, 22, 12, 21, time.UTC), + now: time.Date(2022, 11, 17, 15, 22, 22, 21, time.UTC), + expected: "10s", + }, + { + name: "10 minutes", + t: time.Date(2022, 11, 17, 15, 22, 12, 21, time.UTC), + now: time.Date(2022, 11, 17, 15, 32, 12, 21, time.UTC), + expected: "10m", + }, + { + name: "24 hours", + t: time.Date(2022, 11, 17, 15, 22, 12, 21, time.UTC), + now: time.Date(2022, 11, 18, 15, 22, 12, 21, time.UTC), + expected: "1d", + }, + { + name: "25 hours", + t: time.Date(2022, 11, 17, 15, 22, 12, 21, time.UTC), + now: time.Date(2022, 11, 18, 16, 22, 12, 21, time.UTC), + expected: "1d", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + actual := Age(tt.t, tt.now) + if tt.expected != actual { + t.Errorf("expected %s; got %s", tt.expected, actual) + } + }) + } +} diff --git a/internal/cmd/volume/list.go b/internal/cmd/volume/list.go index c9ec11ce..6d489168 100644 --- a/internal/cmd/volume/list.go +++ b/internal/cmd/volume/list.go @@ -3,6 +3,7 @@ package volume import ( "context" "strings" + "time" "github.com/spf13/cobra" @@ -72,7 +73,7 @@ var listCmd = base.ListCmd{ })). AddFieldFn("age", output.FieldFn(func(obj interface{}) string { volume := obj.(*hcloud.Volume) - return util.Age(volume.Created) + return util.Age(volume.Created, time.Now()) })) }, From 186604952d720488d0fc2c196a78d107080ab006 Mon Sep 17 00:00:00 2001 From: Cedric Kienzler Date: Thu, 17 Nov 2022 15:54:10 +0100 Subject: [PATCH 3/4] make unit-tests more resilient --- internal/cmd/network/list_test.go | 4 +++- internal/cmd/placementgroup/list_test.go | 4 +++- internal/cmd/primaryip/list_test.go | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/cmd/network/list_test.go b/internal/cmd/network/list_test.go index c928ad6a..45e4c8f5 100644 --- a/internal/cmd/network/list_test.go +++ b/internal/cmd/network/list_test.go @@ -4,6 +4,7 @@ import ( "context" "net" "testing" + "time" "github.com/golang/mock/gomock" "github.com/hetznercloud/cli/internal/cmd/network" @@ -36,6 +37,7 @@ func TestList(t *testing.T) { Name: "test-net", IPRange: &net.IPNet{IP: net.ParseIP("192.0.2.1"), Mask: net.CIDRMask(24, 32)}, Servers: []*hcloud.Server{{ID: 3421}}, + Created: time.Now().Add(-10 * time.Second), }, }, nil) @@ -43,7 +45,7 @@ func TestList(t *testing.T) { out, err := fx.Run(cmd, []string{"--selector", "foo=bar"}) expOut := `ID NAME IP RANGE SERVERS AGE -123 test-net 192.0.2.1/24 1 server 106751d +123 test-net 192.0.2.1/24 1 server 10s ` assert.NoError(t, err) diff --git a/internal/cmd/placementgroup/list_test.go b/internal/cmd/placementgroup/list_test.go index 955316da..6f2212b7 100644 --- a/internal/cmd/placementgroup/list_test.go +++ b/internal/cmd/placementgroup/list_test.go @@ -3,6 +3,7 @@ package placementgroup_test import ( "context" "testing" + "time" "github.com/golang/mock/gomock" "github.com/hetznercloud/cli/internal/cmd/placementgroup" @@ -39,13 +40,14 @@ func TestList(t *testing.T) { Labels: map[string]string{"key": "value"}, Servers: []int{4711, 4712}, Type: hcloud.PlacementGroupTypeSpread, + Created: time.Now().Add(-10 * time.Second), }, }, nil) out, err := fx.Run(cmd, []string{"--selector", "foo=bar"}) expOut := `ID NAME SERVERS TYPE AGE -897 my Placement Group 2 servers spread 106751d +897 my Placement Group 2 servers spread 10s ` assert.NoError(t, err) diff --git a/internal/cmd/primaryip/list_test.go b/internal/cmd/primaryip/list_test.go index 89f69dcb..0325bf44 100644 --- a/internal/cmd/primaryip/list_test.go +++ b/internal/cmd/primaryip/list_test.go @@ -4,6 +4,7 @@ import ( "context" "net" "testing" + "time" "github.com/golang/mock/gomock" "github.com/hetznercloud/cli/internal/testutil" @@ -36,6 +37,7 @@ func TestList(t *testing.T) { AutoDelete: true, Type: hcloud.PrimaryIPTypeIPv4, IP: net.ParseIP("127.0.0.1"), + Created: time.Now().Add(-10 * time.Second), }, }, nil) @@ -43,7 +45,7 @@ func TestList(t *testing.T) { out, err := fx.Run(cmd, []string{"--selector", "foo=bar"}) expOut := `ID TYPE NAME IP ASSIGNEE DNS AUTO DELETE AGE -123 ipv4 test-net 127.0.0.1 - - yes 106751d +123 ipv4 test-net 127.0.0.1 - - yes 10s ` assert.NoError(t, err) From 4b06a56bbd5c7573c8b7e876b5825bc6f941bb0e Mon Sep 17 00:00:00 2001 From: cedi Date: Thu, 17 Nov 2022 15:56:14 +0100 Subject: [PATCH 4/4] Update internal/cmd/util/util_internal_test.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julian Tölle --- internal/cmd/util/util_internal_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/util/util_internal_test.go b/internal/cmd/util/util_internal_test.go index 2f8b7fb2..e9b35f7b 100644 --- a/internal/cmd/util/util_internal_test.go +++ b/internal/cmd/util/util_internal_test.go @@ -50,7 +50,7 @@ func TestOnlyOneSet(t *testing.T) { } } -func TestAgo(t *testing.T) { +func TestAge(t *testing.T) { tests := []struct { name string t time.Time