Skip to content

Commit 083cda6

Browse files
authored
Merge pull request #78 from DevopsArtFactory/codex/issue-73-list-sorting
[codex] add alphabetical sorting to AWS list views
2 parents 1e5d615 + 9a8a282 commit 083cda6

11 files changed

Lines changed: 304 additions & 47 deletions

File tree

internal/services/aws/ec2.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package aws
22

33
import (
44
"context"
5+
"sort"
56
"strings"
67

78
awssdk "github.com/aws/aws-sdk-go-v2/aws"
@@ -59,6 +60,15 @@ func (r *AwsRepository) ListRunningInstances(ctx context.Context) ([]EC2Instance
5960
}
6061
}
6162

63+
sort.Slice(instances, func(i, j int) bool {
64+
left := normalizedSortKey(instances[i].Name, instances[i].InstanceID)
65+
right := normalizedSortKey(instances[j].Name, instances[j].InstanceID)
66+
if left == right {
67+
return instances[i].InstanceID < instances[j].InstanceID
68+
}
69+
return left < right
70+
})
71+
6272
return instances, nil
6373
}
6474

internal/services/aws/ec2_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package aws
22

33
import (
4+
"context"
45
"testing"
56

67
"github.com/aws/aws-sdk-go-v2/aws"
8+
"github.com/aws/aws-sdk-go-v2/service/ec2"
79
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
10+
"github.com/aws/aws-sdk-go-v2/service/ssm"
11+
ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
812
)
913

1014
func TestFilterText(t *testing.T) {
@@ -103,6 +107,55 @@ func TestDerefString(t *testing.T) {
103107
}
104108
}
105109

110+
func TestListRunningInstances_SortedByName(t *testing.T) {
111+
ec2Mock := &mockEC2Client{
112+
describeInstancesFunc: func(_ context.Context, _ *ec2.DescribeInstancesInput, _ ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
113+
return &ec2.DescribeInstancesOutput{
114+
Reservations: []types.Reservation{
115+
{
116+
Instances: []types.Instance{
117+
{
118+
InstanceId: aws.String("i-2"),
119+
PrivateIpAddress: aws.String("10.0.0.2"),
120+
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
121+
Tags: []types.Tag{{Key: aws.String("Name"), Value: aws.String("zeta-web")}},
122+
},
123+
{
124+
InstanceId: aws.String("i-1"),
125+
PrivateIpAddress: aws.String("10.0.0.1"),
126+
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
127+
Tags: []types.Tag{{Key: aws.String("Name"), Value: aws.String("alpha-web")}},
128+
},
129+
},
130+
},
131+
},
132+
}, nil
133+
},
134+
}
135+
ssmMock := &mockSSMClient{
136+
describeInstanceInfoFunc: func(_ context.Context, _ *ssm.DescribeInstanceInformationInput, _ ...func(*ssm.Options)) (*ssm.DescribeInstanceInformationOutput, error) {
137+
return &ssm.DescribeInstanceInformationOutput{
138+
InstanceInformationList: []ssmtypes.InstanceInformation{
139+
{InstanceId: aws.String("i-2")},
140+
{InstanceId: aws.String("i-1")},
141+
},
142+
}, nil
143+
},
144+
}
145+
146+
repo := &AwsRepository{EC2Client: ec2Mock, SSMClient: ssmMock}
147+
instances, err := repo.ListRunningInstances(context.Background())
148+
if err != nil {
149+
t.Fatalf("unexpected error: %v", err)
150+
}
151+
if len(instances) != 2 {
152+
t.Fatalf("expected 2 instances, got %d", len(instances))
153+
}
154+
if instances[0].Name != "alpha-web" || instances[1].Name != "zeta-web" {
155+
t.Fatalf("expected alphabetical EC2 order, got %+v", instances)
156+
}
157+
}
158+
106159
func containsStr(s, substr string) bool {
107160
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstr(s, substr))
108161
}

internal/services/aws/rds.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package aws
33
import (
44
"context"
55
"fmt"
6+
"sort"
67

78
awssdk "github.com/aws/aws-sdk-go-v2/aws"
89
"github.com/aws/aws-sdk-go-v2/service/rds"
@@ -38,6 +39,14 @@ func (r *AwsRepository) ListDBInstances(ctx context.Context) ([]RDSInstance, err
3839

3940
instances = append(instances, inst)
4041
}
42+
sort.Slice(instances, func(i, j int) bool {
43+
left := normalizedSortKey(instances[i].DBInstanceID)
44+
right := normalizedSortKey(instances[j].DBInstanceID)
45+
if left == right {
46+
return instances[i].Endpoint < instances[j].Endpoint
47+
}
48+
return left < right
49+
})
4150
return instances, nil
4251
}
4352

internal/services/aws/rds_test.go

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -117,37 +117,40 @@ func TestListDBInstances_Success(t *testing.T) {
117117
}
118118

119119
inst := instances[0]
120-
if inst.DBInstanceID != "my-db" {
121-
t.Errorf("expected DBInstanceID 'my-db', got %q", inst.DBInstanceID)
120+
if inst.DBInstanceID != "aurora-inst-1" {
121+
t.Errorf("expected DBInstanceID 'aurora-inst-1', got %q", inst.DBInstanceID)
122122
}
123-
if inst.Engine != "mysql" {
124-
t.Errorf("expected Engine 'mysql', got %q", inst.Engine)
123+
if inst.Engine != "aurora-mysql" {
124+
t.Errorf("expected Engine 'aurora-mysql', got %q", inst.Engine)
125125
}
126-
if inst.EngineVersion != "8.0.35" {
127-
t.Errorf("expected EngineVersion '8.0.35', got %q", inst.EngineVersion)
126+
if inst.EngineVersion != "8.0.mysql_aurora.3.04.0" {
127+
t.Errorf("expected EngineVersion '8.0.mysql_aurora.3.04.0', got %q", inst.EngineVersion)
128128
}
129129
if inst.Status != "available" {
130130
t.Errorf("expected Status 'available', got %q", inst.Status)
131131
}
132-
if inst.InstanceClass != "db.t3.micro" {
133-
t.Errorf("expected InstanceClass 'db.t3.micro', got %q", inst.InstanceClass)
132+
if inst.InstanceClass != "db.r6g.large" {
133+
t.Errorf("expected InstanceClass 'db.r6g.large', got %q", inst.InstanceClass)
134134
}
135-
if !inst.MultiAZ {
136-
t.Error("expected MultiAZ to be true")
135+
if inst.MultiAZ {
136+
t.Error("expected MultiAZ to be false")
137137
}
138-
if inst.StorageGB != 20 {
139-
t.Errorf("expected StorageGB 20, got %d", inst.StorageGB)
138+
if inst.StorageGB != 0 {
139+
t.Errorf("expected StorageGB 0, got %d", inst.StorageGB)
140140
}
141-
if inst.Endpoint != "my-db.abc123.us-east-1.rds.amazonaws.com:3306" {
141+
if inst.Endpoint != "aurora-inst-1.abc123.us-east-1.rds.amazonaws.com:3306" {
142142
t.Errorf("unexpected Endpoint: %q", inst.Endpoint)
143143
}
144-
if inst.ClusterID != "" {
145-
t.Errorf("expected empty ClusterID, got %q", inst.ClusterID)
144+
if inst.ClusterID != "my-cluster" {
145+
t.Errorf("expected ClusterID 'my-cluster', got %q", inst.ClusterID)
146146
}
147147

148-
aurora := instances[1]
149-
if aurora.ClusterID != "my-cluster" {
150-
t.Errorf("expected ClusterID 'my-cluster', got %q", aurora.ClusterID)
148+
mysql := instances[1]
149+
if mysql.DBInstanceID != "my-db" {
150+
t.Errorf("expected DBInstanceID 'my-db', got %q", mysql.DBInstanceID)
151+
}
152+
if mysql.ClusterID != "" {
153+
t.Errorf("expected empty ClusterID, got %q", mysql.ClusterID)
151154
}
152155
}
153156

@@ -215,6 +218,31 @@ func TestListDBInstances_NilEndpoint(t *testing.T) {
215218
}
216219
}
217220

221+
func TestListDBInstances_SortedByIdentifier(t *testing.T) {
222+
mock := &mockRDSClient{
223+
describeDBInstancesFunc: func(_ context.Context, _ *rds.DescribeDBInstancesInput, _ ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) {
224+
return &rds.DescribeDBInstancesOutput{
225+
DBInstances: []rdstypes.DBInstance{
226+
{DBInstanceIdentifier: awssdk.String("zeta-db")},
227+
{DBInstanceIdentifier: awssdk.String("alpha-db")},
228+
},
229+
}, nil
230+
},
231+
}
232+
233+
repo := &AwsRepository{RDSClient: mock}
234+
instances, err := repo.ListDBInstances(context.Background())
235+
if err != nil {
236+
t.Fatalf("unexpected error: %v", err)
237+
}
238+
if len(instances) != 2 {
239+
t.Fatalf("expected 2 instances, got %d", len(instances))
240+
}
241+
if instances[0].DBInstanceID != "alpha-db" || instances[1].DBInstanceID != "zeta-db" {
242+
t.Fatalf("expected alphabetical DB identifier order, got %+v", instances)
243+
}
244+
}
245+
218246
// --- DescribeDBInstance tests ---
219247

220248
func TestDescribeDBInstance_Success(t *testing.T) {

internal/services/aws/route53.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package aws
33
import (
44
"context"
55
"fmt"
6+
"sort"
67
"strings"
78

89
awssdk "github.com/aws/aws-sdk-go-v2/aws"
@@ -45,6 +46,15 @@ func (r *AwsRepository) ListHostedZones(ctx context.Context) ([]HostedZone, erro
4546
marker = output.NextMarker
4647
}
4748

49+
sort.Slice(zones, func(i, j int) bool {
50+
left := normalizedSortKey(strings.TrimSuffix(zones[i].Name, "."), zones[i].ID)
51+
right := normalizedSortKey(strings.TrimSuffix(zones[j].Name, "."), zones[j].ID)
52+
if left == right {
53+
return zones[i].ID < zones[j].ID
54+
}
55+
return left < right
56+
})
57+
4858
return zones, nil
4959
}
5060

internal/services/aws/route53_test.go

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import (
1313

1414
// mockRoute53Client implements Route53ClientAPI for testing.
1515
type mockRoute53Client struct {
16-
listHostedZonesFunc func(ctx context.Context, params *route53.ListHostedZonesInput, optFns ...func(*route53.Options)) (*route53.ListHostedZonesOutput, error)
17-
listResourceRecordSetsFunc func(ctx context.Context, params *route53.ListResourceRecordSetsInput, optFns ...func(*route53.Options)) (*route53.ListResourceRecordSetsOutput, error)
16+
listHostedZonesFunc func(ctx context.Context, params *route53.ListHostedZonesInput, optFns ...func(*route53.Options)) (*route53.ListHostedZonesOutput, error)
17+
listResourceRecordSetsFunc func(ctx context.Context, params *route53.ListResourceRecordSetsInput, optFns ...func(*route53.Options)) (*route53.ListResourceRecordSetsOutput, error)
1818
changeResourceRecordSetsFunc func(ctx context.Context, params *route53.ChangeResourceRecordSetsInput, optFns ...func(*route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error)
19-
getChangeFunc func(ctx context.Context, params *route53.GetChangeInput, optFns ...func(*route53.Options)) (*route53.GetChangeOutput, error)
19+
getChangeFunc func(ctx context.Context, params *route53.GetChangeInput, optFns ...func(*route53.Options)) (*route53.GetChangeOutput, error)
2020
}
2121

2222
func (m *mockRoute53Client) ListHostedZones(ctx context.Context, params *route53.ListHostedZonesInput, optFns ...func(*route53.Options)) (*route53.ListHostedZonesOutput, error) {
@@ -49,17 +49,17 @@ func TestListHostedZones_Success(t *testing.T) {
4949
return &route53.ListHostedZonesOutput{
5050
HostedZones: []r53types.HostedZone{
5151
{
52-
Id: awssdk.String("/hostedzone/Z1234567890"),
53-
Name: awssdk.String("example.com."),
52+
Id: awssdk.String("/hostedzone/Z1234567890"),
53+
Name: awssdk.String("example.com."),
5454
ResourceRecordSetCount: awssdk.Int64(10),
5555
Config: &r53types.HostedZoneConfig{
5656
PrivateZone: false,
5757
Comment: awssdk.String("Production zone"),
5858
},
5959
},
6060
{
61-
Id: awssdk.String("/hostedzone/Z0987654321"),
62-
Name: awssdk.String("internal.example.com."),
61+
Id: awssdk.String("/hostedzone/Z0987654321"),
62+
Name: awssdk.String("internal.example.com."),
6363
ResourceRecordSetCount: awssdk.Int64(5),
6464
Config: &r53types.HostedZoneConfig{
6565
PrivateZone: true,
@@ -144,10 +144,10 @@ func TestListHostedZones_NilConfig(t *testing.T) {
144144
return &route53.ListHostedZonesOutput{
145145
HostedZones: []r53types.HostedZone{
146146
{
147-
Id: awssdk.String("/hostedzone/Z111"),
148-
Name: awssdk.String("noconfig.com."),
147+
Id: awssdk.String("/hostedzone/Z111"),
148+
Name: awssdk.String("noconfig.com."),
149149
ResourceRecordSetCount: awssdk.Int64(1),
150-
Config: nil,
150+
Config: nil,
151151
},
152152
},
153153
IsTruncated: false,
@@ -171,6 +171,32 @@ func TestListHostedZones_NilConfig(t *testing.T) {
171171
}
172172
}
173173

174+
func TestListHostedZones_SortedByName(t *testing.T) {
175+
mock := &mockRoute53Client{
176+
listHostedZonesFunc: func(_ context.Context, _ *route53.ListHostedZonesInput, _ ...func(*route53.Options)) (*route53.ListHostedZonesOutput, error) {
177+
return &route53.ListHostedZonesOutput{
178+
HostedZones: []r53types.HostedZone{
179+
{Id: awssdk.String("/hostedzone/Z2"), Name: awssdk.String("zeta.example.com.")},
180+
{Id: awssdk.String("/hostedzone/Z1"), Name: awssdk.String("alpha.example.com.")},
181+
},
182+
IsTruncated: false,
183+
}, nil
184+
},
185+
}
186+
187+
repo := &AwsRepository{Route53Client: mock}
188+
zones, err := repo.ListHostedZones(context.Background())
189+
if err != nil {
190+
t.Fatalf("unexpected error: %v", err)
191+
}
192+
if len(zones) != 2 {
193+
t.Fatalf("expected 2 zones, got %d", len(zones))
194+
}
195+
if zones[0].Name != "alpha.example.com." || zones[1].Name != "zeta.example.com." {
196+
t.Fatalf("expected alphabetical hosted zone order, got %+v", zones)
197+
}
198+
}
199+
174200
// --- ListResourceRecordSets tests ---
175201

176202
func TestListResourceRecordSets_Success(t *testing.T) {

internal/services/aws/securitygroup.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package aws
33
import (
44
"context"
55
"fmt"
6+
"sort"
67

78
awssdk "github.com/aws/aws-sdk-go-v2/aws"
89
"github.com/aws/aws-sdk-go-v2/service/ec2"
@@ -65,6 +66,14 @@ func (r *AwsRepository) ListSecurityGroups(ctx context.Context) ([]SecurityGroup
6566
group.EgressRules = parseRules(sg.IpPermissionsEgress)
6667
sgs = append(sgs, group)
6768
}
69+
sort.Slice(sgs, func(i, j int) bool {
70+
left := normalizedSortKey(sgs[i].Name, sgs[i].GroupID)
71+
right := normalizedSortKey(sgs[j].Name, sgs[j].GroupID)
72+
if left == right {
73+
return sgs[i].GroupID < sgs[j].GroupID
74+
}
75+
return left < right
76+
})
6877
return sgs, nil
6978
}
7079

0 commit comments

Comments
 (0)