carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package awsinv
2
3import (
4 "context"
5 "fmt"
6 "strconv"
7 "strings"
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// res builds one Resource with the fields every mapping sets the same way,
16// so a new mapping cannot forget the provider stamp.
17func res(kind, nativeType, id, name, region string) infraview.Resource {
18 return infraview.Resource{
19 Provider: "aws", Kind: kind, NativeType: nativeType,
20 ID: id, Name: name, Region: region, Attrs: map[string]string{},
21 }
22}
23
24// set writes an attribute only when it has a value: an absent key and an
25// empty string are different claims, and the joins treat "" as absence.
26func set(r *infraview.Resource, key, value string) {
27 if value != "" {
28 r.Attrs[key] = value
29 }
30}
31
32func tagsOf(tags []ec2types.Tag) map[string]string {
33 if len(tags) == 0 {
34 return nil
35 }
36 out := map[string]string{}
37 for _, t := range tags {
38 out[aws.ToString(t.Key)] = aws.ToString(t.Value)
39 }
40 return out
41}
42
43// ec2Resources maps one region's EC2 surface. Each call contributes its own
44// resources and, on failure, one line to partial — never an error: this
45// function's whole contract is that a throttled volumes call costs the
46// volumes and nothing else.
47func ec2Resources(ctx context.Context, api EC2API, region string) (out []infraview.Resource, partial []string) {
48 fail := func(what string, err error) {
49 partial = append(partial, fmt.Sprintf("%s in %s: %v", what, region, err))
50 }
51
52 if o, err := api.DescribeInstances(ctx, &ec2.DescribeInstancesInput{}); err != nil {
53 fail("instances", err)
54 } else if o != nil {
55 for _, rv := range o.Reservations {
56 for _, in := range rv.Instances {
57 tags := tagsOf(in.Tags)
58 r := res(infraview.KindMachine, "ec2:instance", aws.ToString(in.InstanceId), tags["Name"], region)
59 r.Tags = tags
60 set(&r, "type", string(in.InstanceType))
61 if in.State != nil {
62 set(&r, "state", string(in.State.Name))
63 }
64 if in.Placement != nil {
65 set(&r, "az", aws.ToString(in.Placement.AvailabilityZone))
66 }
67 set(&r, "public_ip", aws.ToString(in.PublicIpAddress))
68 set(&r, "private_ip", aws.ToString(in.PrivateIpAddress))
69 // Which security groups and which instance role. Both come
70 // back on this same DescribeInstances call — no extra API
71 // call and no extra IAM action — and both are how
72 // /fleet/architecture tells a CARLOS security group and a
73 // CARLOS instance role from the account's other ones
74 // (console/archestate.go). GROUP IDS, not names: the
75 // firewall resource is keyed on the id, so the join is
76 // exact rather than a name match.
77 var groups []string
78 for _, g := range in.SecurityGroups {
79 if id := aws.ToString(g.GroupId); id != "" {
80 groups = append(groups, id)
81 }
82 }
83 set(&r, "security_groups", strings.Join(groups, ","))
84 if in.IamInstanceProfile != nil {
85 set(&r, "iam_profile", aws.ToString(in.IamInstanceProfile.Arn))
86 }
87 if in.LaunchTime != nil {
88 set(&r, "launched", in.LaunchTime.UTC().Format("2006-01-02"))
89 }
90 out = append(out, r)
91 }
92 }
93 }
94
95 if o, err := api.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{}); err != nil {
96 fail("volumes", err)
97 } else if o != nil {
98 for _, v := range o.Volumes {
99 r := res(infraview.KindDisk, "ec2:volume", aws.ToString(v.VolumeId), tagsOf(v.Tags)["Name"], region)
100 r.Tags = tagsOf(v.Tags)
101 if v.Size != nil {
102 set(&r, "size_gb", strconv.Itoa(int(*v.Size)))
103 }
104 set(&r, "volume_type", string(v.VolumeType))
105 for _, a := range v.Attachments {
106 set(&r, "attached_to", aws.ToString(a.InstanceId))
107 }
108 out = append(out, r)
109 }
110 }
111
112 if o, err := api.DescribeAddresses(ctx, &ec2.DescribeAddressesInput{}); err != nil {
113 fail("elastic IPs", err)
114 } else if o != nil {
115 for _, a := range o.Addresses {
116 r := res(infraview.KindIP, "ec2:eip", aws.ToString(a.AllocationId), tagsOf(a.Tags)["Name"], region)
117 r.Tags = tagsOf(a.Tags)
118 set(&r, "address", aws.ToString(a.PublicIp))
119 set(&r, "attached_to", aws.ToString(a.InstanceId))
120 out = append(out, r)
121 }
122 }
123
124 if o, err := api.DescribeSecurityGroups(ctx, &ec2.DescribeSecurityGroupsInput{}); err != nil {
125 fail("security groups", err)
126 } else if o != nil {
127 for _, g := range o.SecurityGroups {
128 r := res(infraview.KindFirewall, "ec2:security-group", aws.ToString(g.GroupId), aws.ToString(g.GroupName), region)
129 r.Tags = tagsOf(g.Tags)
130 set(&r, "ingress", summarizePerms(g.IpPermissions))
131 set(&r, "egress", summarizePerms(g.IpPermissionsEgress))
132 out = append(out, r)
133 }
134 }
135
136 if o, err := api.DescribeVpcs(ctx, &ec2.DescribeVpcsInput{}); err != nil {
137 fail("VPCs", err)
138 } else if o != nil {
139 for _, v := range o.Vpcs {
140 r := res(infraview.KindNetwork, "ec2:vpc", aws.ToString(v.VpcId), tagsOf(v.Tags)["Name"], region)
141 r.Tags = tagsOf(v.Tags)
142 set(&r, "cidr", aws.ToString(v.CidrBlock))
143 out = append(out, r)
144 }
145 }
146
147 if o, err := api.DescribeSubnets(ctx, &ec2.DescribeSubnetsInput{}); err != nil {
148 fail("subnets", err)
149 } else if o != nil {
150 for _, s := range o.Subnets {
151 r := res(infraview.KindNetwork, "ec2:subnet", aws.ToString(s.SubnetId), tagsOf(s.Tags)["Name"], region)
152 r.Tags = tagsOf(s.Tags)
153 set(&r, "cidr", aws.ToString(s.CidrBlock))
154 set(&r, "az", aws.ToString(s.AvailabilityZone))
155 set(&r, "vpc", aws.ToString(s.VpcId))
156 out = append(out, r)
157 }
158 }
159 return out, partial
160}
161
162// summarizePerms renders a rule set as "tcp/22 from 10.0.0.0/8 · tcp/443
163// from 0.0.0.0/0" — a sentence an operator reads, never the raw JSON, which
164// would be both unreadable and a wall of detail the page does not want.
165func summarizePerms(perms []ec2types.IpPermission) string {
166 out := ""
167 for _, p := range perms {
168 proto := aws.ToString(p.IpProtocol)
169 if proto == "-1" {
170 proto = "all"
171 }
172 port := "all"
173 if p.FromPort != nil {
174 port = strconv.Itoa(int(*p.FromPort))
175 if p.ToPort != nil && *p.ToPort != *p.FromPort {
176 port += "-" + strconv.Itoa(int(*p.ToPort))
177 }
178 }
179 for _, rng := range p.IpRanges {
180 if out != "" {
181 out += " · "
182 }
183 out += fmt.Sprintf("%s/%s from %s", proto, port, aws.ToString(rng.CidrIp))
184 }
185 }
186 return out
187}
188