@@ -15,6 +15,292 @@ import (
1515 "github.com/stretchr/testify/require"
1616)
1717
18+ func TestFilterAvailableUpdates (t * testing.T ) {
19+ instances := []instance {
20+ {
21+ instanceID : "inst-id-1" ,
22+ containerInstanceID : "cont-inst-1" ,
23+ },
24+ {
25+ instanceID : "inst-id-2" ,
26+ containerInstanceID : "cont-inst-2" ,
27+ },
28+ {
29+ instanceID : "inst-id-3" ,
30+ containerInstanceID : "cont-inst-3" ,
31+ },
32+ {
33+ instanceID : "inst-id-4" ,
34+ containerInstanceID : "cont-inst-4" ,
35+ },
36+ {
37+ instanceID : "inst-id-5" ,
38+ containerInstanceID : "cont-inst-5" ,
39+ },
40+ }
41+ expected := []instance {
42+ {
43+ instanceID : "inst-id-1" ,
44+ containerInstanceID : "cont-inst-1" ,
45+ bottlerocketVersion : "v1.0.5" ,
46+ },
47+ {
48+ instanceID : "inst-id-2" ,
49+ containerInstanceID : "cont-inst-2" ,
50+ bottlerocketVersion : "v1.0.5" ,
51+ },
52+ {
53+ instanceID : "inst-id-5" ,
54+ containerInstanceID : "cont-inst-5" ,
55+ bottlerocketVersion : "v1.0.5" ,
56+ },
57+ }
58+ responses := map [string ]string {
59+ "inst-id-1" : `{"update_state": "Available", "active_partition": { "image": { "version": "v1.0.5"}}}` ,
60+ "inst-id-2" : `{"update_state": "Ready", "active_partition": { "image": { "version": "v1.0.5"}}}` ,
61+ "inst-id-3" : `{"update_state": "Idle", "active_partition": { "image": { "version": "v1.1.1"}}}` ,
62+ "inst-id-4" : `{"update_state": "Staged", "active_partition": { "image": { "version": "v1.1.1"}}}` ,
63+ "inst-id-5" : `{"update_state": "Available", "active_partition": { "image": { "version": "v1.0.5"}}}` ,
64+ }
65+ sendCommandCalls := 0
66+ commandWaiterCalls := 0
67+ getCommandInvocationCalls := 0
68+ mockSSM := MockSSM {
69+ GetCommandInvocationFn : func (input * ssm.GetCommandInvocationInput ) (* ssm.GetCommandInvocationOutput , error ) {
70+ getCommandInvocationCalls ++
71+ return & ssm.GetCommandInvocationOutput {
72+ Status : aws .String ("Success" ),
73+ StandardOutputContent : aws .String (responses [* input .InstanceId ]),
74+ }, nil
75+ },
76+ SendCommandFn : func (input * ssm.SendCommandInput ) (* ssm.SendCommandOutput , error ) {
77+ sendCommandCalls ++
78+ return & ssm.SendCommandOutput {
79+ Command : & ssm.Command {
80+ CommandId : aws .String ("command-id" ),
81+ DocumentName : aws .String ("check-document" ),
82+ },
83+ }, nil
84+ },
85+ WaitUntilCommandExecutedWithContextFn : func (ctx aws.Context , input * ssm.GetCommandInvocationInput , opts ... request.WaiterOption ) error {
86+ commandWaiterCalls ++
87+ assert .Equal (t , "command-id" , aws .StringValue (input .CommandId ))
88+ return nil
89+ },
90+ }
91+ u := updater {ssm : mockSSM , checkDocument : "check-document" }
92+ actual , err := u .filterAvailableUpdates (instances )
93+ require .NoError (t , err )
94+ assert .Equal (t , expected , actual , "Should only contain instances in Aavailable or Ready update state" )
95+ assert .Equal (t , 1 , sendCommandCalls , "should send commands for each page" )
96+ assert .Equal (t , 5 , commandWaiterCalls , "should wait for each instance" )
97+ assert .Equal (t , 5 , getCommandInvocationCalls , "should collect output for each instance" )
98+ }
99+
100+ func TestPaginatedFilterAvailableUpdatesSuccess (t * testing.T ) {
101+ checkPattern := `{"update_state": "%s", "active_partition": { "image": { "version": "%s"}}}`
102+ expected := make ([]instance , 0 )
103+ instances := make ([]instance , 0 )
104+ getOut := & ssm.GetCommandInvocationOutput {
105+ Status : aws .String ("Success" ),
106+ StandardOutputContent : aws .String (fmt .Sprintf (checkPattern , updateStateAvailable , "v1.0.5" )),
107+ }
108+
109+ for i := 0 ; i < 100 ; i ++ { // 100 is chosen here to reprsent 2 full pages of SSM (limited to 50 per page)
110+ containerID := "cont-inst-br" + strconv .Itoa (i )
111+ ec2ID := "ec2-id-br" + strconv .Itoa (i )
112+ instances = append (instances , instance {
113+ instanceID : ec2ID ,
114+ containerInstanceID : containerID ,
115+ })
116+ expected = append (expected , instance {
117+ instanceID : ec2ID ,
118+ containerInstanceID : containerID ,
119+ bottlerocketVersion : "v1.0.5" ,
120+ })
121+ }
122+
123+ sendCommandCalls := 0
124+ commandWaiterCalls := 0
125+ getCommandInvocationCalls := 0
126+ mockSSM := MockSSM {
127+ GetCommandInvocationFn : func (input * ssm.GetCommandInvocationInput ) (* ssm.GetCommandInvocationOutput , error ) {
128+ getCommandInvocationCalls ++
129+ return getOut , nil
130+ },
131+ SendCommandFn : func (input * ssm.SendCommandInput ) (* ssm.SendCommandOutput , error ) {
132+ sendCommandCalls ++
133+ return & ssm.SendCommandOutput {
134+ Command : & ssm.Command {
135+ CommandId : aws .String ("command-id" ),
136+ DocumentName : aws .String ("check-document" ),
137+ },
138+ }, nil
139+ },
140+ WaitUntilCommandExecutedWithContextFn : func (ctx aws.Context , input * ssm.GetCommandInvocationInput , opts ... request.WaiterOption ) error {
141+ commandWaiterCalls ++
142+ assert .Equal (t , "command-id" , aws .StringValue (input .CommandId ))
143+ return nil
144+ },
145+ }
146+ u := updater {ssm : mockSSM }
147+ actual , err := u .filterAvailableUpdates (instances )
148+ require .NoError (t , err )
149+ assert .EqualValues (t , expected , actual , "should contain all instances" )
150+ assert .Equal (t , 2 , sendCommandCalls , "should send commands for each page" )
151+ assert .Equal (t , 100 , commandWaiterCalls , "should wait for each instance" )
152+ assert .Equal (t , 100 , getCommandInvocationCalls , "should collect output for each instance" )
153+ }
154+
155+ func TestPaginatedFilterAvailableUpdatesAllFail (t * testing.T ) {
156+ instances := make ([]instance , 0 )
157+
158+ for i := 0 ; i < 100 ; i ++ {
159+ containerID := "cont-inst-br" + strconv .Itoa (i )
160+ ec2ID := "ec2-id-br" + strconv .Itoa (i )
161+ instances = append (instances , instance {
162+ instanceID : ec2ID ,
163+ containerInstanceID : containerID ,
164+ })
165+ }
166+
167+ sendCommandCalls := 0
168+ mockSSM := MockSSM {
169+ SendCommandFn : func (input * ssm.SendCommandInput ) (* ssm.SendCommandOutput , error ) {
170+ sendCommandCalls ++
171+ return nil , errors .New ("Failed to send document" )
172+ },
173+ }
174+ u := updater {ssm : mockSSM }
175+ actual , err := u .filterAvailableUpdates (instances )
176+ require .Error (t , err )
177+ assert .Contains (t , err .Error (), "Failed to send document" )
178+ assert .Empty (t , actual )
179+ assert .Equal (t , 2 , sendCommandCalls , "should send commands for each page" )
180+ }
181+
182+ func TestPaginatedFilterAvailableUpdatesInPageFailures (t * testing.T ) {
183+ instances := make ([]instance , 0 )
184+ checkPattern := `{"update_state": "%s", "active_partition": { "image": { "version": "%s"}}}`
185+ for i := 0 ; i < 120 ; i ++ { // 120 chosen here to ensure multiple pages are tested and that number instances divides by 3 evenly
186+ containerID := "cont-inst-br" + strconv .Itoa (i )
187+ ec2ID := "ec2-id-br" + strconv .Itoa (i )
188+ instances = append (instances , instance {
189+ instanceID : ec2ID ,
190+ containerInstanceID : containerID ,
191+ })
192+ }
193+
194+ sendCommandCalls := 0
195+ commandWaiterCalls := 0
196+ getCommandInvocationCalls := 0
197+ count := 0
198+ mockSSM := MockSSM {
199+ GetCommandInvocationFn : func (input * ssm.GetCommandInvocationInput ) (* ssm.GetCommandInvocationOutput , error ) {
200+ count ++
201+ getCommandInvocationCalls ++
202+ switch count % 3 {
203+ case 0 :
204+ return nil , errors .New ("Failed to get command output" ) // validate getCommandResult failure
205+ case 1 :
206+ return & ssm.GetCommandInvocationOutput {
207+ Status : aws .String ("Success" ),
208+ StandardOutputContent : aws .String ("{}" ),
209+ }, nil // validates parseCommandOutput failure
210+ case 2 :
211+ return & ssm.GetCommandInvocationOutput {
212+ Status : aws .String ("Success" ),
213+ StandardOutputContent : aws .String (fmt .Sprintf (checkPattern , updateStateAvailable , "v1.0.5" )),
214+ }, nil // validate success case
215+ }
216+ return nil , nil
217+ },
218+ SendCommandFn : func (input * ssm.SendCommandInput ) (* ssm.SendCommandOutput , error ) {
219+ sendCommandCalls ++
220+ return & ssm.SendCommandOutput {
221+ Command : & ssm.Command {
222+ CommandId : aws .String ("command-id" ),
223+ DocumentName : aws .String ("check-document" ),
224+ },
225+ }, nil
226+ },
227+ WaitUntilCommandExecutedWithContextFn : func (ctx aws.Context , input * ssm.GetCommandInvocationInput , opts ... request.WaiterOption ) error {
228+ assert .Equal (t , "command-id" , aws .StringValue (input .CommandId ))
229+ commandWaiterCalls ++
230+ return nil
231+ },
232+ }
233+ u := updater {ssm : mockSSM }
234+ actual , err := u .filterAvailableUpdates (instances )
235+ require .NoError (t , err )
236+ assert .EqualValues (t , 40 , len (actual ), "Every 3rd instance of 120 should succeed" )
237+ assert .Equal (t , 3 , sendCommandCalls , "should send commands for each page" )
238+ assert .Equal (t , 120 , commandWaiterCalls , "should wait for each instance" )
239+ assert .Equal (t , 120 , getCommandInvocationCalls , "should collect output for each instance" )
240+ }
241+
242+ func TestPaginatedFilterAvailableUpdatesSingleErr (t * testing.T ) {
243+ checkPattern := `{"update_state": "%s", "active_partition": { "image": { "version": "%s"}}}`
244+ expected := make ([]instance , 0 )
245+ instances := make ([]instance , 0 )
246+ getOut := & ssm.GetCommandInvocationOutput {
247+ Status : aws .String ("Success" ),
248+ StandardOutputContent : aws .String (fmt .Sprintf (checkPattern , updateStateAvailable , "v1.0.5" )),
249+ }
250+
251+ for i := 0 ; i < 100 ; i ++ {
252+ containerID := "cont-inst-br" + strconv .Itoa (i )
253+ ec2ID := "ec2-id-br" + strconv .Itoa (i )
254+ instances = append (instances , instance {
255+ instanceID : ec2ID ,
256+ containerInstanceID : containerID ,
257+ })
258+ expected = append (expected , instance {
259+ instanceID : ec2ID ,
260+ containerInstanceID : containerID ,
261+ bottlerocketVersion : "v1.0.5" ,
262+ })
263+ }
264+
265+ pageErrors := []error {errors .New ("Failed to send document" ), nil }
266+
267+ sendCommandCalls := 0
268+ commandWaiterCalls := 0
269+ getCommandInvocationCalls := 0
270+ callCount := 0
271+ mockSSM := MockSSM {
272+ GetCommandInvocationFn : func (input * ssm.GetCommandInvocationInput ) (* ssm.GetCommandInvocationOutput , error ) {
273+ getCommandInvocationCalls ++
274+ return getOut , nil
275+ },
276+ SendCommandFn : func (input * ssm.SendCommandInput ) (* ssm.SendCommandOutput , error ) {
277+ require .Less (t , callCount , len (pageErrors ))
278+ failErr := pageErrors [callCount ]
279+ callCount ++
280+ sendCommandCalls ++
281+ return & ssm.SendCommandOutput {
282+ Command : & ssm.Command {
283+ CommandId : aws .String ("command-id" ),
284+ DocumentName : aws .String ("check-document" ),
285+ },
286+ }, failErr
287+ },
288+ WaitUntilCommandExecutedWithContextFn : func (ctx aws.Context , input * ssm.GetCommandInvocationInput , opts ... request.WaiterOption ) error {
289+ assert .Equal (t , "command-id" , aws .StringValue (input .CommandId ))
290+ commandWaiterCalls ++
291+ return nil
292+ },
293+ }
294+ u := updater {ssm : mockSSM }
295+ actual , err := u .filterAvailableUpdates (instances )
296+
297+ require .NoError (t , err )
298+ assert .EqualValues (t , actual , expected [50 :], "Should only contain instances from the 2nd page" )
299+ assert .Equal (t , 2 , sendCommandCalls , "should send commands for each page" )
300+ assert .Equal (t , 50 , commandWaiterCalls , "should wait for each instance" )
301+ assert .Equal (t , 50 , getCommandInvocationCalls , "should collect output for each instance" )
302+ }
303+
18304func TestGetCommandResult (t * testing.T ) {
19305 cases := []struct {
20306 name string
0 commit comments