carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package termfence
2
3import (
4 "context"
5 "errors"
6 "testing"
7 "time"
8
9 "github.com/aws/aws-sdk-go-v2/aws"
10 awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
11 "github.com/aws/aws-sdk-go-v2/service/ec2"
12 "github.com/aws/aws-sdk-go-v2/service/ec2/types"
13 "github.com/aws/smithy-go"
14 smithymiddleware "github.com/aws/smithy-go/middleware"
15
16 "github.com/carlosframework/platform/internal/boxid"
17)
18
19var (
20 box = Box{Ref: "annie-welding", Enrollment: "enr-annie-1",
21 Cloud: boxid.Cloud{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}}
22 worker = Box{Ref: "bert-sawing", Enrollment: "enr-bert-1",
23 Cloud: boxid.Cloud{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0bbb"}}
24 now = time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
25)
26
27func describe(owner, instance string, state types.InstanceStateName) *ec2.DescribeInstancesOutput {
28 return &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{
29 OwnerId: aws.String(owner),
30 Instances: []types.Instance{{InstanceId: aws.String(instance), State: &types.InstanceState{Name: state}}},
31 }}}
32}
33
34func withRequestID(out *ec2.DescribeInstancesOutput, id string) *ec2.DescribeInstancesOutput {
35 var md smithymiddleware.Metadata
36 awsmiddleware.SetRequestIDMetadata(&md, id)
37 out.ResultMetadata = md
38 return out
39}
40
41func TestATerminatedInstanceOfThisBoxIsAFence(t *testing.T) {
42 out := withRequestID(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "req-1")
43 rec, err := Verify(box, worker, FromDescribe(out, "i-0abc", "eu-west-1"), now)
44 if err != nil {
45 t.Fatal(err)
46 }
47 want := Record{V: 1, Box: "annie-welding", Enrollment: "enr-annie-1", Cloud: box.Cloud, State: "terminated",
48 ObservedAt: "2026-10-06T12:00:00Z", Via: "describe", RequestID: "req-1",
49 WrittenBy: WrittenBy{Box: "bert-sawing", Enrollment: "enr-bert-1"}}
50 if rec != want {
51 t.Fatalf("record = %+v\nwant %+v", rec, want)
52 }
53}
54
55// Every lookalike is refused.
56func TestLookalikesAreNotEvidence(t *testing.T) {
57 cases := map[string]Observation{
58 "stopped": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameStopped), "i-0abc", "eu-west-1"),
59 "stopping": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameStopping), "i-0abc", "eu-west-1"),
60 "shutting-down": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameShuttingDown), "i-0abc", "eu-west-1"),
61 "running": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameRunning), "i-0abc", "eu-west-1"),
62 "another account": FromDescribe(describe("999999999999", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1"),
63 "another region": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "us-east-1"),
64 "another instance": FromDescribe(describe("111122223333", "i-0zzz", types.InstanceStateNameTerminated), "i-0zzz", "eu-west-1"),
65 "not in the reply": FromDescribe(describe("111122223333", "i-0zzz", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1"),
66 "empty reply": FromDescribe(&ec2.DescribeInstancesOutput{}, "i-0abc", "eu-west-1"),
67 "nil reply": FromDescribe(nil, "i-0abc", "eu-west-1"),
68 "no state": FromDescribe(&ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{OwnerId: aws.String("111122223333"), Instances: []types.Instance{{InstanceId: aws.String("i-0abc")}}}}}, "i-0abc", "eu-west-1"),
69 "unknown source": {Via: "cloudtrail", Found: true, InstanceID: "i-0abc", Account: "111122223333", Region: "eu-west-1", State: "terminated"},
70 "zero observation": {},
71 }
72 for name, o := range cases {
73 t.Run(name, func(t *testing.T) {
74 if rec, err := Verify(box, worker, o, now); !errors.Is(err, ErrNotEvidence) {
75 t.Fatalf("record = %+v, err = %v; want ErrNotEvidence", rec, err)
76 }
77 })
78 }
79}
80
81// Review Focus 5: the fence is a regional worker's own read in the box's
82// own region. A worker enrolled elsewhere is refused even when what it
83// read is a perfect terminated observation.
84func TestAWorkerInAnotherRegionNeverFences(t *testing.T) {
85 far := worker
86 far.Cloud.Region = "us-east-1"
87 o := FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1")
88 if _, err := Verify(box, far, o, now); !errors.Is(err, ErrNotEvidence) {
89 t.Fatalf("err = %v, want ErrNotEvidence", err)
90 }
91}
92
93func TestAnIncompleteRosterEntryIsNeverFenced(t *testing.T) {
94 // A found, terminated observation whose every coordinate is empty, so
95 // it MATCHES an empty cloud field for field. Only the guard on the
96 // entry itself can refuse it.
97 o := Observation{Via: ViaDescribe, Found: true, State: "terminated"}
98 for name, b := range map[string]Box{
99 "empty cloud": {Ref: "annie-welding", Enrollment: "enr-annie-1"},
100 "no enrollment": {Ref: "annie-welding", Cloud: box.Cloud},
101 "no reference": {Enrollment: "enr-annie-1", Cloud: box.Cloud},
102 "not aws": {Ref: "annie-welding", Enrollment: "enr-annie-1", Cloud: boxid.Cloud{Provider: "gcp", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}},
103 } {
104 t.Run(name, func(t *testing.T) {
105 full := Observation{Via: ViaDescribe, Found: true, InstanceID: b.Cloud.InstanceID, Account: b.Cloud.Account, Region: b.Cloud.Region, State: "terminated"}
106 for _, obs := range []Observation{o, full} {
107 if _, err := Verify(b, worker, obs, now); !errors.Is(err, ErrNotEvidence) {
108 t.Fatalf("err = %v, want ErrNotEvidence", err)
109 }
110 }
111 })
112 }
113 if _, err := Verify(box, Box{Ref: "bert-sawing"}, FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1"), now); !errors.Is(err, ErrNotEvidence) {
114 t.Fatalf("a worker with no proven identity: err = %v", err)
115 }
116}
117
118const terminatedEvent = `{"version":"0","id":"ev-123","detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","time":"2026-10-06T11:59:00Z","region":"eu-west-1","resources":["arn:aws:ec2:eu-west-1:111122223333:instance/i-0abc"],"detail":{"instance-id":"i-0abc","state":"terminated"}}`
119
120func TestATerminatedEventIsAFence(t *testing.T) {
121 o, err := FromEvent([]byte(terminatedEvent))
122 if err != nil {
123 t.Fatal(err)
124 }
125 rec, err := Verify(box, worker, o, now)
126 if err != nil {
127 t.Fatal(err)
128 }
129 if rec.Via != "event" || rec.EventID != "ev-123" || rec.State != "terminated" || rec.RequestID != "" {
130 t.Fatalf("record = %+v", rec)
131 }
132}
133
134func TestEventsThatAreNotEC2StateChangesAreRefused(t *testing.T) {
135 for name, body := range map[string]string{
136 "not JSON": `terminated`,
137 "another source": `{"id":"e","detail-type":"EC2 Instance State-change Notification","source":"my.app","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"terminated"}}`,
138 "another detail-type": `{"id":"e","detail-type":"EC2 Spot Instance Interruption Warning","source":"aws.ec2","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"terminated"}}`,
139 "no event id": `{"detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"terminated"}}`,
140 } {
141 t.Run(name, func(t *testing.T) {
142 if _, err := FromEvent([]byte(body)); err == nil {
143 t.Fatal("accepted")
144 }
145 })
146 }
147}
148
149func TestAShuttingDownEventIsNotEvidence(t *testing.T) {
150 body := `{"id":"e","detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"shutting-down"}}`
151 o, err := FromEvent([]byte(body))
152 if err != nil {
153 t.Fatal(err)
154 }
155 if _, err := Verify(box, worker, o, now); !errors.Is(err, ErrNotEvidence) {
156 t.Fatalf("err = %v, want ErrNotEvidence", err)
157 }
158}
159
160// fakeEC2 answers DescribeInstances and records the region each call was
161// forced into.
162type fakeEC2 struct {
163 out *ec2.DescribeInstancesOutput
164 err error
165 regions []string
166 ids [][]string
167}
168
169func (f *fakeEC2) DescribeInstances(_ context.Context, in *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
170 o := ec2.Options{Region: "client-default"}
171 for _, fn := range optFns {
172 fn(&o)
173 }
174 f.regions = append(f.regions, o.Region)
175 f.ids = append(f.ids, in.InstanceIds)
176 return f.out, f.err
177}
178
179func TestDescribeInstanceAsksInTheBoxsOwnRegion(t *testing.T) {
180 f := &fakeEC2{out: describe("111122223333", "i-0abc", types.InstanceStateNameTerminated)}
181 out, err := DescribeInstance(context.Background(), f, box.Cloud)
182 if err != nil {
183 t.Fatal(err)
184 }
185 if len(f.regions) != 1 || f.regions[0] != "eu-west-1" || len(f.ids[0]) != 1 || f.ids[0][0] != "i-0abc" {
186 t.Fatalf("called in %v for %v", f.regions, f.ids)
187 }
188 if _, err := Verify(box, worker, FromDescribe(out, "i-0abc", "eu-west-1"), now); err != nil {
189 t.Fatal(err)
190 }
191}
192
193func TestDescribeInstanceNotFoundIsAbsenceAndOtherErrorsAreErrors(t *testing.T) {
194 notFound := &fakeEC2{err: &smithy.GenericAPIError{Code: "InvalidInstanceID.NotFound", Message: "gone"}}
195 out, err := DescribeInstance(context.Background(), notFound, box.Cloud)
196 if err != nil {
197 t.Fatalf("NotFound: err = %v", err)
198 }
199 if _, err := Verify(box, worker, FromDescribe(out, "i-0abc", "eu-west-1"), now); !errors.Is(err, ErrNotEvidence) {
200 t.Fatalf("NotFound fenced the box: err = %v", err)
201 }
202 denied := &fakeEC2{err: &smithy.GenericAPIError{Code: "UnauthorizedOperation", Message: "no"}}
203 if _, err := DescribeInstance(context.Background(), denied, box.Cloud); err == nil {
204 t.Fatal("an access error read as an answer")
205 }
206 if _, err := DescribeInstance(context.Background(), &fakeEC2{}, boxid.Cloud{InstanceID: "i-0abc"}); err == nil {
207 t.Fatal("described an incomplete cloud")
208 }
209}
210