| 1 | package awsinv |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | |
| 9 | "github.com/aws/aws-sdk-go-v2/aws" |
| 10 | "github.com/aws/aws-sdk-go-v2/service/ec2" |
| 11 | ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" |
| 12 | "github.com/carlosframework/platform/internal/infraview" |
| 13 | ) |
| 14 | |
| 15 | // fakeEC2 answers whatever the test hands it; a non-nil err field for a |
| 16 | // call makes that ONE call fail, which is how the partial-failure rule |
| 17 | // below is proven. |
| 18 | type fakeEC2 struct { |
| 19 | instances *ec2.DescribeInstancesOutput |
| 20 | volumes *ec2.DescribeVolumesOutput |
| 21 | addresses *ec2.DescribeAddressesOutput |
| 22 | groups *ec2.DescribeSecurityGroupsOutput |
| 23 | vpcs *ec2.DescribeVpcsOutput |
| 24 | subnets *ec2.DescribeSubnetsOutput |
| 25 | regions *ec2.DescribeRegionsOutput |
| 26 | errVols error |
| 27 | } |
| 28 | |
| 29 | func (f *fakeEC2) DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) { |
| 30 | return f.instances, nil |
| 31 | } |
| 32 | func (f *fakeEC2) DescribeVolumes(context.Context, *ec2.DescribeVolumesInput, ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error) { |
| 33 | if f.errVols != nil { |
| 34 | return nil, f.errVols |
| 35 | } |
| 36 | return f.volumes, nil |
| 37 | } |
| 38 | func (f *fakeEC2) DescribeAddresses(context.Context, *ec2.DescribeAddressesInput, ...func(*ec2.Options)) (*ec2.DescribeAddressesOutput, error) { |
| 39 | return f.addresses, nil |
| 40 | } |
| 41 | func (f *fakeEC2) DescribeSecurityGroups(context.Context, *ec2.DescribeSecurityGroupsInput, ...func(*ec2.Options)) (*ec2.DescribeSecurityGroupsOutput, error) { |
| 42 | return f.groups, nil |
| 43 | } |
| 44 | func (f *fakeEC2) DescribeVpcs(context.Context, *ec2.DescribeVpcsInput, ...func(*ec2.Options)) (*ec2.DescribeVpcsOutput, error) { |
| 45 | return f.vpcs, nil |
| 46 | } |
| 47 | func (f *fakeEC2) DescribeSubnets(context.Context, *ec2.DescribeSubnetsInput, ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error) { |
| 48 | return f.subnets, nil |
| 49 | } |
| 50 | func (f *fakeEC2) DescribeRegions(context.Context, *ec2.DescribeRegionsInput, ...func(*ec2.Options)) (*ec2.DescribeRegionsOutput, error) { |
| 51 | return f.regions, nil |
| 52 | } |
| 53 | |
| 54 | func find(rs []infraview.Resource, kind, id string) *infraview.Resource { |
| 55 | for i := range rs { |
| 56 | if rs[i].Kind == kind && rs[i].ID == id { |
| 57 | return &rs[i] |
| 58 | } |
| 59 | } |
| 60 | return nil |
| 61 | } |
| 62 | |
| 63 | func TestEC2InstanceMapping(t *testing.T) { |
| 64 | f := &fakeEC2{instances: &ec2.DescribeInstancesOutput{ |
| 65 | Reservations: []ec2types.Reservation{{Instances: []ec2types.Instance{{ |
| 66 | InstanceId: aws.String("i-123"), |
| 67 | InstanceType: ec2types.InstanceTypeT4gMedium, |
| 68 | State: &ec2types.InstanceState{Name: ec2types.InstanceStateNameRunning}, |
| 69 | Placement: &ec2types.Placement{AvailabilityZone: aws.String("eu-west-1a")}, |
| 70 | PublicIpAddress: aws.String("52.0.0.1"), |
| 71 | PrivateIpAddress: aws.String("10.0.0.5"), |
| 72 | Tags: []ec2types.Tag{{Key: aws.String("Name"), Value: aws.String("carlos-eu-west-1")}}, |
| 73 | }}}}, |
| 74 | }} |
| 75 | |
| 76 | rs, partial := ec2Resources(context.Background(), f, "eu-west-1") |
| 77 | if len(partial) != 0 { |
| 78 | t.Fatalf("partial = %v, want none", partial) |
| 79 | } |
| 80 | m := find(rs, infraview.KindMachine, "i-123") |
| 81 | if m == nil { |
| 82 | t.Fatalf("no machine in %+v", rs) |
| 83 | } |
| 84 | if m.Name != "carlos-eu-west-1" { |
| 85 | t.Errorf("Name = %q, want the Name tag", m.Name) |
| 86 | } |
| 87 | if m.Region != "eu-west-1" || m.NativeType != "ec2:instance" { |
| 88 | t.Errorf("region/type = %q/%q", m.Region, m.NativeType) |
| 89 | } |
| 90 | for k, want := range map[string]string{ |
| 91 | "type": "t4g.medium", "state": "running", "az": "eu-west-1a", |
| 92 | "public_ip": "52.0.0.1", "private_ip": "10.0.0.5", |
| 93 | } { |
| 94 | if got := m.Attr(k); got != want { |
| 95 | t.Errorf("attr %s = %q, want %q", k, got, want) |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | // One failing call costs its own resources and nothing else — the rule the |
| 101 | // whole inventory depends on, since a page that shows eleven of twelve |
| 102 | // classes is worth far more than one that shows none. |
| 103 | func TestEC2PartialFailureCostsOnlyItsOwnCall(t *testing.T) { |
| 104 | f := &fakeEC2{ |
| 105 | instances: &ec2.DescribeInstancesOutput{Reservations: []ec2types.Reservation{{ |
| 106 | Instances: []ec2types.Instance{{InstanceId: aws.String("i-123")}}, |
| 107 | }}}, |
| 108 | errVols: errors.New("Throttling: rate exceeded"), |
| 109 | } |
| 110 | rs, partial := ec2Resources(context.Background(), f, "eu-west-1") |
| 111 | if find(rs, infraview.KindMachine, "i-123") == nil { |
| 112 | t.Fatalf("a failed volume read must not cost the instances") |
| 113 | } |
| 114 | if len(partial) != 1 { |
| 115 | t.Fatalf("partial = %v, want exactly the volumes line", partial) |
| 116 | } |
| 117 | if !strings.Contains(partial[0], "volumes") || !strings.Contains(partial[0], "eu-west-1") { |
| 118 | t.Errorf("partial line %q must name what failed and where", partial[0]) |
| 119 | } |
| 120 | } |
| 121 | |