| 1 | // Package awsinv discovers what an AWS account is carrying and reports it |
| 2 | // as an internal/infraview.Inventory. It is the ONLY package in this repo |
| 3 | // that imports an AWS SDK for inventory purposes, which is the whole point |
| 4 | // of the seam: the console page that renders an estate never sees an AWS |
| 5 | // type, so a second provider is a new package rather than a rewrite. |
| 6 | // |
| 7 | // Every call here is read-only. The IAM policy this package needs |
| 8 | // (infra/modules/region-host/main.tf) contains no write action, so that |
| 9 | // property is enforced by the credential and not by this comment. |
| 10 | package awsinv |
| 11 | |
| 12 | import ( |
| 13 | "context" |
| 14 | |
| 15 | "github.com/aws/aws-sdk-go-v2/aws" |
| 16 | "github.com/aws/aws-sdk-go-v2/service/acm" |
| 17 | "github.com/aws/aws-sdk-go-v2/service/cloudwatch" |
| 18 | "github.com/aws/aws-sdk-go-v2/service/ec2" |
| 19 | "github.com/aws/aws-sdk-go-v2/service/iam" |
| 20 | "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi" |
| 21 | "github.com/aws/aws-sdk-go-v2/service/route53" |
| 22 | "github.com/aws/aws-sdk-go-v2/service/s3" |
| 23 | ) |
| 24 | |
| 25 | // The narrow interfaces exist so the mappings can be tested against fakes |
| 26 | // with no network and no credential. Each lists exactly the calls this |
| 27 | // package makes and nothing else — an interface that named the whole |
| 28 | // client would be an invitation to reach for a write. |
| 29 | type EC2API interface { |
| 30 | DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) |
| 31 | DescribeVolumes(context.Context, *ec2.DescribeVolumesInput, ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error) |
| 32 | DescribeAddresses(context.Context, *ec2.DescribeAddressesInput, ...func(*ec2.Options)) (*ec2.DescribeAddressesOutput, error) |
| 33 | DescribeSecurityGroups(context.Context, *ec2.DescribeSecurityGroupsInput, ...func(*ec2.Options)) (*ec2.DescribeSecurityGroupsOutput, error) |
| 34 | DescribeVpcs(context.Context, *ec2.DescribeVpcsInput, ...func(*ec2.Options)) (*ec2.DescribeVpcsOutput, error) |
| 35 | DescribeSubnets(context.Context, *ec2.DescribeSubnetsInput, ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error) |
| 36 | DescribeRegions(context.Context, *ec2.DescribeRegionsInput, ...func(*ec2.Options)) (*ec2.DescribeRegionsOutput, error) |
| 37 | } |
| 38 | |
| 39 | type S3API interface { |
| 40 | ListBuckets(context.Context, *s3.ListBucketsInput, ...func(*s3.Options)) (*s3.ListBucketsOutput, error) |
| 41 | GetBucketLocation(context.Context, *s3.GetBucketLocationInput, ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error) |
| 42 | GetBucketVersioning(context.Context, *s3.GetBucketVersioningInput, ...func(*s3.Options)) (*s3.GetBucketVersioningOutput, error) |
| 43 | GetBucketEncryption(context.Context, *s3.GetBucketEncryptionInput, ...func(*s3.Options)) (*s3.GetBucketEncryptionOutput, error) |
| 44 | GetPublicAccessBlock(context.Context, *s3.GetPublicAccessBlockInput, ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) |
| 45 | } |
| 46 | |
| 47 | type Route53API interface { |
| 48 | ListHostedZones(context.Context, *route53.ListHostedZonesInput, ...func(*route53.Options)) (*route53.ListHostedZonesOutput, error) |
| 49 | ListResourceRecordSets(context.Context, *route53.ListResourceRecordSetsInput, ...func(*route53.Options)) (*route53.ListResourceRecordSetsOutput, error) |
| 50 | } |
| 51 | |
| 52 | type IAMAPI interface { |
| 53 | ListRoles(context.Context, *iam.ListRolesInput, ...func(*iam.Options)) (*iam.ListRolesOutput, error) |
| 54 | ListUsers(context.Context, *iam.ListUsersInput, ...func(*iam.Options)) (*iam.ListUsersOutput, error) |
| 55 | ListAttachedRolePolicies(context.Context, *iam.ListAttachedRolePoliciesInput, ...func(*iam.Options)) (*iam.ListAttachedRolePoliciesOutput, error) |
| 56 | } |
| 57 | |
| 58 | type CloudWatchAPI interface { |
| 59 | DescribeAlarms(context.Context, *cloudwatch.DescribeAlarmsInput, ...func(*cloudwatch.Options)) (*cloudwatch.DescribeAlarmsOutput, error) |
| 60 | } |
| 61 | |
| 62 | type ACMAPI interface { |
| 63 | ListCertificates(context.Context, *acm.ListCertificatesInput, ...func(*acm.Options)) (*acm.ListCertificatesOutput, error) |
| 64 | } |
| 65 | |
| 66 | type TaggingAPI interface { |
| 67 | GetResources(context.Context, *resourcegroupstaggingapi.GetResourcesInput, ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) |
| 68 | } |
| 69 | |
| 70 | // ec2Methods satisfies EC2API from method values of one ec2 client. |
| 71 | type ec2Methods struct { |
| 72 | describeInstances func(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) |
| 73 | describeVolumes func(context.Context, *ec2.DescribeVolumesInput, ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error) |
| 74 | describeAddresses func(context.Context, *ec2.DescribeAddressesInput, ...func(*ec2.Options)) (*ec2.DescribeAddressesOutput, error) |
| 75 | describeSecurityGroups func(context.Context, *ec2.DescribeSecurityGroupsInput, ...func(*ec2.Options)) (*ec2.DescribeSecurityGroupsOutput, error) |
| 76 | describeVpcs func(context.Context, *ec2.DescribeVpcsInput, ...func(*ec2.Options)) (*ec2.DescribeVpcsOutput, error) |
| 77 | describeSubnets func(context.Context, *ec2.DescribeSubnetsInput, ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error) |
| 78 | describeRegions func(context.Context, *ec2.DescribeRegionsInput, ...func(*ec2.Options)) (*ec2.DescribeRegionsOutput, error) |
| 79 | } |
| 80 | |
| 81 | func (m ec2Methods) DescribeInstances(ctx context.Context, in *ec2.DescribeInstancesInput, opts ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) { |
| 82 | return m.describeInstances(ctx, in, opts...) |
| 83 | } |
| 84 | |
| 85 | func (m ec2Methods) DescribeVolumes(ctx context.Context, in *ec2.DescribeVolumesInput, opts ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error) { |
| 86 | return m.describeVolumes(ctx, in, opts...) |
| 87 | } |
| 88 | |
| 89 | func (m ec2Methods) DescribeAddresses(ctx context.Context, in *ec2.DescribeAddressesInput, opts ...func(*ec2.Options)) (*ec2.DescribeAddressesOutput, error) { |
| 90 | return m.describeAddresses(ctx, in, opts...) |
| 91 | } |
| 92 | |
| 93 | func (m ec2Methods) DescribeSecurityGroups(ctx context.Context, in *ec2.DescribeSecurityGroupsInput, opts ...func(*ec2.Options)) (*ec2.DescribeSecurityGroupsOutput, error) { |
| 94 | return m.describeSecurityGroups(ctx, in, opts...) |
| 95 | } |
| 96 | |
| 97 | func (m ec2Methods) DescribeVpcs(ctx context.Context, in *ec2.DescribeVpcsInput, opts ...func(*ec2.Options)) (*ec2.DescribeVpcsOutput, error) { |
| 98 | return m.describeVpcs(ctx, in, opts...) |
| 99 | } |
| 100 | |
| 101 | func (m ec2Methods) DescribeSubnets(ctx context.Context, in *ec2.DescribeSubnetsInput, opts ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error) { |
| 102 | return m.describeSubnets(ctx, in, opts...) |
| 103 | } |
| 104 | |
| 105 | func (m ec2Methods) DescribeRegions(ctx context.Context, in *ec2.DescribeRegionsInput, opts ...func(*ec2.Options)) (*ec2.DescribeRegionsOutput, error) { |
| 106 | return m.describeRegions(ctx, in, opts...) |
| 107 | } |
| 108 | |
| 109 | func newEc2Methods(c *ec2.Client) ec2Methods { |
| 110 | return ec2Methods{ |
| 111 | describeInstances: c.DescribeInstances, |
| 112 | describeVolumes: c.DescribeVolumes, |
| 113 | describeAddresses: c.DescribeAddresses, |
| 114 | describeSecurityGroups: c.DescribeSecurityGroups, |
| 115 | describeVpcs: c.DescribeVpcs, |
| 116 | describeSubnets: c.DescribeSubnets, |
| 117 | describeRegions: c.DescribeRegions, |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | // s3Methods satisfies S3API from method values of one s3 client. |
| 122 | type s3Methods struct { |
| 123 | listBuckets func(context.Context, *s3.ListBucketsInput, ...func(*s3.Options)) (*s3.ListBucketsOutput, error) |
| 124 | getBucketLocation func(context.Context, *s3.GetBucketLocationInput, ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error) |
| 125 | getBucketVersioning func(context.Context, *s3.GetBucketVersioningInput, ...func(*s3.Options)) (*s3.GetBucketVersioningOutput, error) |
| 126 | getBucketEncryption func(context.Context, *s3.GetBucketEncryptionInput, ...func(*s3.Options)) (*s3.GetBucketEncryptionOutput, error) |
| 127 | getPublicAccessBlock func(context.Context, *s3.GetPublicAccessBlockInput, ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) |
| 128 | } |
| 129 | |
| 130 | func (m s3Methods) ListBuckets(ctx context.Context, in *s3.ListBucketsInput, opts ...func(*s3.Options)) (*s3.ListBucketsOutput, error) { |
| 131 | return m.listBuckets(ctx, in, opts...) |
| 132 | } |
| 133 | |
| 134 | func (m s3Methods) GetBucketLocation(ctx context.Context, in *s3.GetBucketLocationInput, opts ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error) { |
| 135 | return m.getBucketLocation(ctx, in, opts...) |
| 136 | } |
| 137 | |
| 138 | func (m s3Methods) GetBucketVersioning(ctx context.Context, in *s3.GetBucketVersioningInput, opts ...func(*s3.Options)) (*s3.GetBucketVersioningOutput, error) { |
| 139 | return m.getBucketVersioning(ctx, in, opts...) |
| 140 | } |
| 141 | |
| 142 | func (m s3Methods) GetBucketEncryption(ctx context.Context, in *s3.GetBucketEncryptionInput, opts ...func(*s3.Options)) (*s3.GetBucketEncryptionOutput, error) { |
| 143 | return m.getBucketEncryption(ctx, in, opts...) |
| 144 | } |
| 145 | |
| 146 | func (m s3Methods) GetPublicAccessBlock(ctx context.Context, in *s3.GetPublicAccessBlockInput, opts ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) { |
| 147 | return m.getPublicAccessBlock(ctx, in, opts...) |
| 148 | } |
| 149 | |
| 150 | func newS3Methods(c *s3.Client) s3Methods { |
| 151 | return s3Methods{ |
| 152 | listBuckets: c.ListBuckets, |
| 153 | getBucketLocation: c.GetBucketLocation, |
| 154 | getBucketVersioning: c.GetBucketVersioning, |
| 155 | getBucketEncryption: c.GetBucketEncryption, |
| 156 | getPublicAccessBlock: c.GetPublicAccessBlock, |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | // route53Methods satisfies Route53API from method values of one route53 client. |
| 161 | type route53Methods struct { |
| 162 | listHostedZones func(context.Context, *route53.ListHostedZonesInput, ...func(*route53.Options)) (*route53.ListHostedZonesOutput, error) |
| 163 | listResourceRecordSets func(context.Context, *route53.ListResourceRecordSetsInput, ...func(*route53.Options)) (*route53.ListResourceRecordSetsOutput, error) |
| 164 | } |
| 165 | |
| 166 | func (m route53Methods) ListHostedZones(ctx context.Context, in *route53.ListHostedZonesInput, opts ...func(*route53.Options)) (*route53.ListHostedZonesOutput, error) { |
| 167 | return m.listHostedZones(ctx, in, opts...) |
| 168 | } |
| 169 | |
| 170 | func (m route53Methods) ListResourceRecordSets(ctx context.Context, in *route53.ListResourceRecordSetsInput, opts ...func(*route53.Options)) (*route53.ListResourceRecordSetsOutput, error) { |
| 171 | return m.listResourceRecordSets(ctx, in, opts...) |
| 172 | } |
| 173 | |
| 174 | func newRoute53Methods(c *route53.Client) route53Methods { |
| 175 | return route53Methods{ |
| 176 | listHostedZones: c.ListHostedZones, |
| 177 | listResourceRecordSets: c.ListResourceRecordSets, |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | // iamMethods satisfies IAMAPI from method values of one iam client. |
| 182 | type iamMethods struct { |
| 183 | listRoles func(context.Context, *iam.ListRolesInput, ...func(*iam.Options)) (*iam.ListRolesOutput, error) |
| 184 | listUsers func(context.Context, *iam.ListUsersInput, ...func(*iam.Options)) (*iam.ListUsersOutput, error) |
| 185 | listAttachedRolePolicies func(context.Context, *iam.ListAttachedRolePoliciesInput, ...func(*iam.Options)) (*iam.ListAttachedRolePoliciesOutput, error) |
| 186 | } |
| 187 | |
| 188 | func (m iamMethods) ListRoles(ctx context.Context, in *iam.ListRolesInput, opts ...func(*iam.Options)) (*iam.ListRolesOutput, error) { |
| 189 | return m.listRoles(ctx, in, opts...) |
| 190 | } |
| 191 | |
| 192 | func (m iamMethods) ListUsers(ctx context.Context, in *iam.ListUsersInput, opts ...func(*iam.Options)) (*iam.ListUsersOutput, error) { |
| 193 | return m.listUsers(ctx, in, opts...) |
| 194 | } |
| 195 | |
| 196 | func (m iamMethods) ListAttachedRolePolicies(ctx context.Context, in *iam.ListAttachedRolePoliciesInput, opts ...func(*iam.Options)) (*iam.ListAttachedRolePoliciesOutput, error) { |
| 197 | return m.listAttachedRolePolicies(ctx, in, opts...) |
| 198 | } |
| 199 | |
| 200 | func newIamMethods(c *iam.Client) iamMethods { |
| 201 | return iamMethods{ |
| 202 | listRoles: c.ListRoles, |
| 203 | listUsers: c.ListUsers, |
| 204 | listAttachedRolePolicies: c.ListAttachedRolePolicies, |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | // cloudwatchMethods satisfies CloudWatchAPI from method values of one cloudwatch client. |
| 209 | type cloudwatchMethods struct { |
| 210 | describeAlarms func(context.Context, *cloudwatch.DescribeAlarmsInput, ...func(*cloudwatch.Options)) (*cloudwatch.DescribeAlarmsOutput, error) |
| 211 | } |
| 212 | |
| 213 | func (m cloudwatchMethods) DescribeAlarms(ctx context.Context, in *cloudwatch.DescribeAlarmsInput, opts ...func(*cloudwatch.Options)) (*cloudwatch.DescribeAlarmsOutput, error) { |
| 214 | return m.describeAlarms(ctx, in, opts...) |
| 215 | } |
| 216 | |
| 217 | func newCloudwatchMethods(c *cloudwatch.Client) cloudwatchMethods { |
| 218 | return cloudwatchMethods{ |
| 219 | describeAlarms: c.DescribeAlarms, |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | // acmMethods satisfies ACMAPI from method values of one acm client. |
| 224 | type acmMethods struct { |
| 225 | listCertificates func(context.Context, *acm.ListCertificatesInput, ...func(*acm.Options)) (*acm.ListCertificatesOutput, error) |
| 226 | } |
| 227 | |
| 228 | func (m acmMethods) ListCertificates(ctx context.Context, in *acm.ListCertificatesInput, opts ...func(*acm.Options)) (*acm.ListCertificatesOutput, error) { |
| 229 | return m.listCertificates(ctx, in, opts...) |
| 230 | } |
| 231 | |
| 232 | func newAcmMethods(c *acm.Client) acmMethods { |
| 233 | return acmMethods{ |
| 234 | listCertificates: c.ListCertificates, |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | // resourcegroupstaggingapiMethods satisfies TaggingAPI from method values of one resourcegroupstaggingapi client. |
| 239 | type resourcegroupstaggingapiMethods struct { |
| 240 | getResources func(context.Context, *resourcegroupstaggingapi.GetResourcesInput, ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) |
| 241 | } |
| 242 | |
| 243 | func (m resourcegroupstaggingapiMethods) GetResources(ctx context.Context, in *resourcegroupstaggingapi.GetResourcesInput, opts ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) { |
| 244 | return m.getResources(ctx, in, opts...) |
| 245 | } |
| 246 | |
| 247 | func newResourcegroupstaggingapiMethods(c *resourcegroupstaggingapi.Client) resourcegroupstaggingapiMethods { |
| 248 | return resourcegroupstaggingapiMethods{ |
| 249 | getResources: c.GetResources, |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | // Clients is one region's worth of read-only clients. S3/Route53/IAM are |
| 254 | // global services; the assembly calls them once, from the home region's |
| 255 | // Clients, rather than once per region. |
| 256 | type Clients struct { |
| 257 | EC2 EC2API |
| 258 | S3 S3API |
| 259 | Route53 Route53API |
| 260 | IAM IAMAPI |
| 261 | CloudWatch CloudWatchAPI |
| 262 | ACM ACMAPI |
| 263 | Tagging TaggingAPI |
| 264 | } |
| 265 | |
| 266 | // Factory builds the clients for one region. Tests inject a fake; the real |
| 267 | // one below rebases a single resolved aws.Config onto each region so the |
| 268 | // credential chain resolves exactly once. |
| 269 | type Factory func(region string) Clients |
| 270 | |
| 271 | // factoryFor stores METHOD VALUES, not the SDK clients, in the interfaces. |
| 272 | // html/template (the edge and console pages) switches the linker into a |
| 273 | // mode that keeps every exported method of any concrete type stored in an |
| 274 | // interface value. A *ec2.Client in an interface kept all ~2,400 EC2 |
| 275 | // methods (14 MB), and the other clients kept theirs too: the linux/arm64 |
| 276 | // binary doubled to 77.7 MB (discussion #275). A func value bound to one |
| 277 | // method keeps that method and nothing else. Do not "simplify" these back |
| 278 | // to ec2.NewFromConfig(c) in the struct; the CI size ceiling will say so. |
| 279 | func factoryFor(cfg aws.Config) Factory { |
| 280 | return func(region string) Clients { |
| 281 | c := cfg.Copy() |
| 282 | if region != "" { |
| 283 | c.Region = region |
| 284 | } |
| 285 | return Clients{ |
| 286 | EC2: newEc2Methods(ec2.NewFromConfig(c)), |
| 287 | S3: newS3Methods(s3.NewFromConfig(c)), |
| 288 | Route53: newRoute53Methods(route53.NewFromConfig(c)), |
| 289 | IAM: newIamMethods(iam.NewFromConfig(c)), |
| 290 | CloudWatch: newCloudwatchMethods(cloudwatch.NewFromConfig(c)), |
| 291 | ACM: newAcmMethods(acm.NewFromConfig(c)), |
| 292 | Tagging: newResourcegroupstaggingapiMethods(resourcegroupstaggingapi.NewFromConfig(c)), |
| 293 | } |
| 294 | } |
| 295 | } |
| 296 | |