carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package awsmembership is the AWS adapter of the console's membership
2// read (region HA spec 2 §5 step 5, §25; H16, H28): read-only Auto Scaling
3// and EC2 describe calls, made in the AWS region the roster names for the
4// attempt's placement. The console role needs
5// autoscaling:DescribeAutoScalingInstances and ec2:DescribeInstances,
6// conditioned on aws:RequestedRegion being one of the deployment's AWS
7// regions. No box role has either.
8package awsmembership
9
10import (
11 "context"
12 "errors"
13
14 "github.com/aws/aws-sdk-go-v2/aws"
15 awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
16 awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
17 "github.com/aws/aws-sdk-go-v2/config"
18 "github.com/aws/aws-sdk-go-v2/service/autoscaling"
19 "github.com/aws/aws-sdk-go-v2/service/ec2"
20 ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
21 "github.com/aws/smithy-go/middleware"
22
23 "github.com/carlosframework/platform/internal/hostactors"
24)
25
26// Reader is hostactors.MembershipReader over the console's own credentials.
27type Reader struct{ cfg aws.Config }
28
29// New loads the console's AWS configuration (its instance role).
30func New(ctx context.Context) (*Reader, error) {
31 cfg, err := config.LoadDefaultConfig(ctx)
32 if err != nil {
33 return nil, err
34 }
35 return &Reader{cfg: cfg}, nil
36}
37
38// Member describes instance's Auto Scaling membership, then its EC2 state
39// and owner: two calls, two request ids. A call that fails ends the read;
40// the ids of the calls made come back with the error, the failed call's
41// from the cloud's error response (failedID).
42func (r *Reader) Member(ctx context.Context, awsRegion, instance string) (hostactors.Membership, error) {
43 asg := autoscaling.NewFromConfig(r.cfg, func(o *autoscaling.Options) { o.Region = awsRegion })
44 ao, err := asg.DescribeAutoScalingInstances(ctx, &autoscaling.DescribeAutoScalingInstancesInput{InstanceIds: []string{instance}})
45 if err != nil {
46 return hostactors.Membership{RequestIDs: []string{failedID(err)}}, err
47 }
48 ec := ec2.NewFromConfig(r.cfg, func(o *ec2.Options) { o.Region = awsRegion })
49 eo, err := ec.DescribeInstances(ctx, &ec2.DescribeInstancesInput{Filters: []ec2types.Filter{{Name: aws.String("instance-id"), Values: []string{instance}}}})
50 if err != nil {
51 return hostactors.Membership{RequestIDs: []string{requestID(ao.ResultMetadata), failedID(err)}}, err
52 }
53 return membershipFrom(instance, ao, eo), nil
54}
55
56// failedID is the request id of a call that failed: the one the cloud gave
57// in its error response, or hostactors.NoRequestID when the call failed
58// before the cloud answered (no connection, a cancelled context).
59func failedID(err error) string {
60 var re *awshttp.ResponseError
61 if errors.As(err, &re) && re.ServiceRequestID() != "" {
62 return re.ServiceRequestID()
63 }
64 return hostactors.NoRequestID
65}
66
67// membershipFrom reads the two replies. Anything but exactly the one
68// instance asked about, in exactly one group, is no membership.
69func membershipFrom(instance string, ao *autoscaling.DescribeAutoScalingInstancesOutput, eo *ec2.DescribeInstancesOutput) hostactors.Membership {
70 m := hostactors.Membership{RequestIDs: []string{requestID(ao.ResultMetadata), requestID(eo.ResultMetadata)}}
71 if len(ao.AutoScalingInstances) == 1 && aws.ToString(ao.AutoScalingInstances[0].InstanceId) == instance {
72 d := ao.AutoScalingInstances[0]
73 m.GroupName = aws.ToString(d.AutoScalingGroupName)
74 m.LifecycleState = aws.ToString(d.LifecycleState)
75 m.AvailabilityZoneRegion = zoneRegion(aws.ToString(d.AvailabilityZone))
76 }
77 if len(eo.Reservations) == 1 && len(eo.Reservations[0].Instances) == 1 && aws.ToString(eo.Reservations[0].Instances[0].InstanceId) == instance {
78 res := eo.Reservations[0]
79 m.Owner = aws.ToString(res.OwnerId)
80 if st := res.Instances[0].State; st != nil {
81 m.InstanceState = string(st.Name)
82 }
83 }
84 return m
85}
86
87// zoneRegion is the region of an availability zone name ("eu-west-1a" is
88// in "eu-west-1"): the name less one trailing letter after a digit. Any
89// other shape (a Local Zone, a zone id) answers "", which matches no
90// region.
91func zoneRegion(az string) string {
92 n := len(az)
93 if n < 3 || az[n-1] < 'a' || az[n-1] > 'z' || az[n-2] < '0' || az[n-2] > '9' {
94 return ""
95 }
96 return az[:n-1]
97}
98
99func requestID(md middleware.Metadata) string {
100 id, _ := awsmiddleware.GetRequestIDMetadata(md)
101 return id
102}
103
104var _ hostactors.MembershipReader = (*Reader)(nil)
105