@@ -25,6 +25,7 @@ import (
2525 "io"
2626 "net/http"
2727 "net/http/httptest"
28+ "strings"
2829 "testing"
2930 "time"
3031
@@ -59,6 +60,9 @@ const (
5960 tcLatencyFaultExistsCommandOutput = `[{"kind":"netem","handle":"10:","parent":"1:1","options":{"limit":1000,"delay":{"delay":123456789,"jitter":4567,"correlation":0},"ecn":false,"gap":0}}]`
6061 tcLossFaultExistsCommandOutput = `[{"kind":"netem","handle":"10:","dev":"eth0","parent":"1:1","options":{"limit":1000,"loss-random":{"loss":0.06,"correlation":0},"ecn":false,"gap":0}}]`
6162 tcCommandEmptyOutput = `[]`
63+ startEndpoint = "/api/%s/fault/v1/%s/start"
64+ stopEndpoint = "/api/%s/fault/v1/%s/stop"
65+ path = "/some/path"
6266)
6367
6468var (
7680
7781 happyNetworkNamespaces = []* state.NetworkNamespace {
7882 {
79- Path : "/some/ path" ,
83+ Path : path ,
8084 NetworkInterfaces : happyNetworkInterfaces ,
8185 },
8286 }
@@ -2027,9 +2031,13 @@ func generateStopNetworkPacketLossTestCases() []networkFaultInjectionTestCase {
20272031 setExecExpectations : func (exec * mock_execwrapper.MockExec , ctrl * gomock.Controller ) {
20282032 ctx , cancel := context .WithTimeout (context .Background (), ctxTimeoutDuration )
20292033 mockCMD := mock_execwrapper .NewMockCmd (ctrl )
2030- exec .EXPECT ().NewExecContextWithTimeout (gomock .Any (), gomock .Any ()).Times (1 ).Return (ctx , cancel )
2031- exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (mockCMD )
2032- mockCMD .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcLatencyFaultExistsCommandOutput ), nil )
2034+ gomock .InOrder (
2035+ exec .EXPECT ().NewExecContextWithTimeout (gomock .Any (), gomock .Any ()).Times (1 ).Return (ctx , cancel ),
2036+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (mockCMD ),
2037+ mockCMD .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcLossFaultExistsCommandOutput ), nil ),
2038+ )
2039+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (2 ).Return (mockCMD )
2040+ mockCMD .EXPECT ().CombinedOutput ().Times (2 ).Return ([]byte ("" ), nil )
20332041 },
20342042 },
20352043 {
@@ -2175,3 +2183,297 @@ func TestCheckNetworkPacketLoss(t *testing.T) {
21752183 tcs := generateCheckNetworkPacketLossTestCases ()
21762184 testNetworkFaultInjectionCommon (t , tcs , NetworkFaultPath (types .PacketLossFaultType , types .CheckNetworkFaultPostfix ))
21772185}
2186+
2187+ func TestNetworkFaultRequestOrdering (t * testing.T ) {
2188+ tcs := []struct {
2189+ name string
2190+ faultType string
2191+ requestBody interface {}
2192+ setAgentStateExpectations func (agentState * mock_state.MockAgentState , netConfigClient * netconfig.NetworkConfigClient )
2193+ setExecExpectations func (exec * mock_execwrapper.MockExec , ctrl * gomock.Controller , firstStartExecCmd , firstStopExecCmd []interface {})
2194+ }{
2195+ {
2196+ name : types .BlackHolePortFaultType + "request ordering" ,
2197+ faultType : types .BlackHolePortFaultType ,
2198+ requestBody : happyBlackHolePortReqBody ,
2199+ setAgentStateExpectations : func (agentState * mock_state.MockAgentState , netConfigClient * netconfig.NetworkConfigClient ) {
2200+ agentState .EXPECT ().GetTaskMetadataWithTaskNetworkConfig (endpointId , netConfigClient ).
2201+ Return (happyTaskResponse , nil ).
2202+ Times (2 )
2203+ },
2204+ setExecExpectations : func (exec * mock_execwrapper.MockExec , ctrl * gomock.Controller , firstStartExecCmd , firstStopExecCmd []interface {}) {
2205+ startCtx , startCancel := context .WithTimeout (context .Background (), ctxTimeoutDuration )
2206+ stopCtx , stopCancel := context .WithTimeout (context .Background (), ctxTimeoutDuration )
2207+ cmdExec := mock_execwrapper .NewMockCmd (ctrl )
2208+ // We want to ensure that the start fault request executes and finishes first before the stop fault request.
2209+ // We can enforce the ordering of exec mock calls.
2210+ gomock .InOrder (
2211+ // Exec mocks for start black hole port request
2212+ exec .EXPECT ().NewExecContextWithTimeout (gomock .Any (), gomock .Any ()).Do (func (_ , _ interface {}) {
2213+ // Sleep for 2 seconds to mock that the request is taking some time
2214+ time .Sleep (2 * time .Second )
2215+ }).Times (1 ).Return (startCtx , startCancel ),
2216+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2217+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (iptablesChainNotFoundError ), errors .New ("exit status 1" )),
2218+ exec .EXPECT ().ConvertToExitError (gomock .Any ()).Times (1 ).Return (nil , true ),
2219+ exec .EXPECT ().GetExitCode (gomock .Any ()).Times (1 ).Return (1 ),
2220+ // Ensuring that the start request is running here by also passing in the expected parameters for the first CommandContext call
2221+ exec .EXPECT ().CommandContext (gomock .Any (), firstStartExecCmd [0 ], firstStartExecCmd [1 :]... ).Times (1 ).Return (cmdExec ),
2222+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte {}, nil ),
2223+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2224+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte {}, nil ),
2225+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2226+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte {}, nil ),
2227+
2228+ // Exec mocks for stop black hole port request
2229+ exec .EXPECT ().NewExecContextWithTimeout (gomock .Any (), gomock .Any ()).Times (1 ).Return (stopCtx , stopCancel ),
2230+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2231+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte {}, nil ),
2232+ // Ensuring that the stop request is running here by also passing in the expected parameters for the first CommandContext call
2233+ exec .EXPECT ().CommandContext (gomock .Any (), firstStopExecCmd [0 ], firstStopExecCmd [1 :]... ).Times (1 ).Return (cmdExec ),
2234+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte {}, nil ),
2235+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2236+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte {}, nil ),
2237+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2238+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte {}, nil ),
2239+ )
2240+ },
2241+ },
2242+ {
2243+ name : types .LatencyFaultType + "request ordering" ,
2244+ faultType : types .LatencyFaultType ,
2245+ requestBody : happyNetworkLatencyReqBody ,
2246+ setAgentStateExpectations : func (agentState * mock_state.MockAgentState , netConfigClient * netconfig.NetworkConfigClient ) {
2247+ agentState .EXPECT ().GetTaskMetadataWithTaskNetworkConfig (endpointId , netConfigClient ).
2248+ Return (happyTaskResponse , nil ).
2249+ Times (2 )
2250+ },
2251+ setExecExpectations : func (exec * mock_execwrapper.MockExec , ctrl * gomock.Controller , firstStartExecCmd , firstStopExecCmd []interface {}) {
2252+ startCtx , startCancel := context .WithTimeout (context .Background (), ctxTimeoutDuration )
2253+ stopCtx , stopCancel := context .WithTimeout (context .Background (), ctxTimeoutDuration )
2254+ cmdExec := mock_execwrapper .NewMockCmd (ctrl )
2255+ // We want to ensure that the start fault request executes and finishes first before the stop fault request.
2256+ // We can enforce the ordering of exec mock calls.
2257+ gomock .InOrder (
2258+ // Exec mocks for start latency request
2259+ exec .EXPECT ().NewExecContextWithTimeout (gomock .Any (), gomock .Any ()).Do (func (_ , _ interface {}) {
2260+ // Sleep for 2 seconds to mock that the request is taking some time
2261+ time .Sleep (2 * time .Second )
2262+ }).Times (1 ).Return (startCtx , startCancel ),
2263+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2264+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcCommandEmptyOutput ), nil ),
2265+ // Ensuring that the start request is running here by also passing in the expected parameters for the first CommandContext call
2266+ exec .EXPECT ().CommandContext (gomock .Any (), firstStartExecCmd [0 ], firstStartExecCmd [1 :]... ).Times (1 ).Return (cmdExec ),
2267+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcCommandEmptyOutput ), nil ),
2268+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2269+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcCommandEmptyOutput ), nil ),
2270+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2271+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcCommandEmptyOutput ), nil ),
2272+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2273+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcCommandEmptyOutput ), nil ),
2274+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2275+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcCommandEmptyOutput ), nil ),
2276+
2277+ // Exec mocks for stop latency request
2278+ exec .EXPECT ().NewExecContextWithTimeout (gomock .Any (), gomock .Any ()).Times (1 ).Return (stopCtx , stopCancel ),
2279+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2280+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcLatencyFaultExistsCommandOutput ), nil ),
2281+ // Ensuring that the stop request is running here by also passing in the expected parameters for the first CommandContext call
2282+ exec .EXPECT ().CommandContext (gomock .Any (), firstStopExecCmd [0 ], firstStopExecCmd [1 :]... ).Times (1 ).Return (cmdExec ),
2283+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte ("" ), nil ),
2284+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2285+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte ("" ), nil ),
2286+ )
2287+ },
2288+ },
2289+ {
2290+ name : types .PacketLossFaultType + "request ordering" ,
2291+ faultType : types .PacketLossFaultType ,
2292+ requestBody : happyNetworkPacketLossReqBody ,
2293+ setAgentStateExpectations : func (agentState * mock_state.MockAgentState , netConfigClient * netconfig.NetworkConfigClient ) {
2294+ agentState .EXPECT ().GetTaskMetadataWithTaskNetworkConfig (endpointId , netConfigClient ).
2295+ Return (happyTaskResponse , nil ).
2296+ Times (2 )
2297+ },
2298+ setExecExpectations : func (exec * mock_execwrapper.MockExec , ctrl * gomock.Controller , firstStartExecCmd , firstStopExecCmd []interface {}) {
2299+ startCtx , startCancel := context .WithTimeout (context .Background (), ctxTimeoutDuration )
2300+ stopCtx , stopCancel := context .WithTimeout (context .Background (), ctxTimeoutDuration )
2301+ cmdExec := mock_execwrapper .NewMockCmd (ctrl )
2302+ // We want to ensure that the start fault request executes and finishes first before the stop fault request.
2303+ // We can enforce the ordering of exec mock calls.
2304+ gomock .InOrder (
2305+ // Exec mocks for start packet loss request
2306+ exec .EXPECT ().NewExecContextWithTimeout (gomock .Any (), gomock .Any ()).Do (func (_ , _ interface {}) {
2307+ // Sleep for 2 seconds to mock that the request is taking some time
2308+ time .Sleep (2 * time .Second )
2309+ }).Times (1 ).Return (startCtx , startCancel ),
2310+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2311+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcCommandEmptyOutput ), nil ),
2312+ // Ensuring that the start request is running here by also passing in the expected parameters for the first CommandContext call
2313+ exec .EXPECT ().CommandContext (gomock .Any (), firstStartExecCmd [0 ], firstStartExecCmd [1 :]... ).Times (1 ).Return (cmdExec ),
2314+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcCommandEmptyOutput ), nil ),
2315+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2316+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcCommandEmptyOutput ), nil ),
2317+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2318+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcCommandEmptyOutput ), nil ),
2319+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2320+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcCommandEmptyOutput ), nil ),
2321+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2322+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcCommandEmptyOutput ), nil ),
2323+
2324+ // Exec mocks for stop packet loss request
2325+ exec .EXPECT ().NewExecContextWithTimeout (gomock .Any (), gomock .Any ()).Times (1 ).Return (stopCtx , stopCancel ),
2326+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2327+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte (tcLossFaultExistsCommandOutput ), nil ),
2328+ // Ensuring that the stop request is running here by also passing in the expected parameters for the first CommandContext call
2329+ exec .EXPECT ().CommandContext (gomock .Any (), firstStopExecCmd [0 ], firstStopExecCmd [1 :]... ).Times (1 ).Return (cmdExec ),
2330+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte ("" ), nil ),
2331+ exec .EXPECT ().CommandContext (gomock .Any (), gomock .Any (), gomock .Any ()).Times (1 ).Return (cmdExec ),
2332+ cmdExec .EXPECT ().CombinedOutput ().Times (1 ).Return ([]byte ("" ), nil ),
2333+ )
2334+ },
2335+ },
2336+ }
2337+
2338+ for _ , tc := range tcs {
2339+ t .Run (tc .name , func (t * testing.T ) {
2340+ // Mocks
2341+ ctrl := gomock .NewController (t )
2342+ defer ctrl .Finish ()
2343+
2344+ agentState := mock_state .NewMockAgentState (ctrl )
2345+ metricsFactory := mock_metrics .NewMockEntryFactory (ctrl )
2346+
2347+ router := mux .NewRouter ()
2348+ mockExec := mock_execwrapper .NewMockExec (ctrl )
2349+ handler := New (agentState , metricsFactory , mockExec )
2350+ networkConfigClient := netconfig .NewNetworkConfigClient ()
2351+
2352+ var startHandleMethod , stopHandleMethod func (http.ResponseWriter , * http.Request )
2353+ var firstStartExecCmd , firstStopExecCmd []string
2354+ nsenterPrefix := fmt .Sprintf (nsenterCommandString , path )
2355+ switch tc .faultType {
2356+ case types .BlackHolePortFaultType :
2357+ chain := fmt .Sprintf ("%s-%s-%d" , trafficType , protocol , port )
2358+
2359+ newChainCmdString := nsenterPrefix + fmt .Sprintf (iptablesNewChainCmd , requestTimeoutSeconds , chain )
2360+ firstStartExecCmd = strings .Split (newChainCmdString , " " )
2361+
2362+ clearChainCmdString := nsenterPrefix + fmt .Sprintf (iptablesClearChainCmd , requestTimeoutSeconds , chain )
2363+ firstStopExecCmd = strings .Split (clearChainCmdString , " " )
2364+
2365+ startHandleMethod = handler .StartNetworkBlackholePort ()
2366+ stopHandleMethod = handler .StopNetworkBlackHolePort ()
2367+ case types .LatencyFaultType :
2368+ tcAddQdiscRootCommandComposed := nsenterPrefix + fmt .Sprintf (tcAddQdiscRootCommandString , deviceName )
2369+ firstStartExecCmd = strings .Split (tcAddQdiscRootCommandComposed , " " )
2370+
2371+ tcDeleteQdiscParentCommandComposed := nsenterPrefix + fmt .Sprintf (tcDeleteQdiscParentCommandString , deviceName )
2372+ firstStopExecCmd = strings .Split (tcDeleteQdiscParentCommandComposed , " " )
2373+
2374+ startHandleMethod = handler .StartNetworkLatency ()
2375+ stopHandleMethod = handler .StopNetworkLatency ()
2376+ case types .PacketLossFaultType :
2377+ tcAddQdiscRootCommandComposed := nsenterPrefix + fmt .Sprintf (tcAddQdiscRootCommandString , deviceName )
2378+ firstStartExecCmd = strings .Split (tcAddQdiscRootCommandComposed , " " )
2379+
2380+ tcDeleteQdiscParentCommandComposed := nsenterPrefix + fmt .Sprintf (tcDeleteQdiscParentCommandString , deviceName )
2381+ firstStopExecCmd = strings .Split (tcDeleteQdiscParentCommandComposed , " " )
2382+
2383+ startHandleMethod = handler .StartNetworkPacketLoss ()
2384+ stopHandleMethod = handler .StopNetworkPacketLoss ()
2385+ default :
2386+ t .Error ("Unrecognized network fault type" )
2387+ }
2388+
2389+ tc .setAgentStateExpectations (agentState , networkConfigClient )
2390+ tc .setExecExpectations (mockExec , ctrl , convertToInterfaceList (firstStartExecCmd ), convertToInterfaceList (firstStopExecCmd ))
2391+
2392+ router .HandleFunc (
2393+ NetworkFaultPath (tc .faultType , types .StartNetworkFaultPostfix ),
2394+ startHandleMethod ,
2395+ ).Methods (http .MethodPost )
2396+
2397+ router .HandleFunc (
2398+ NetworkFaultPath (tc .faultType , types .StopNetworkFaultPostfix ),
2399+ stopHandleMethod ,
2400+ ).Methods (http .MethodPost )
2401+
2402+ var requestBody io.Reader
2403+ reqBodyBytes , err := json .Marshal (tc .requestBody )
2404+ require .NoError (t , err )
2405+ requestBody = bytes .NewReader (reqBodyBytes )
2406+ startReq , err := http .NewRequest (http .MethodPost , fmt .Sprintf (startEndpoint , endpointId , tc .faultType ), requestBody )
2407+ require .NoError (t , err )
2408+
2409+ ch1 := make (chan struct {
2410+ int
2411+ error
2412+ })
2413+
2414+ reqBodyBytes , err = json .Marshal (tc .requestBody )
2415+ require .NoError (t , err )
2416+ requestBody = bytes .NewReader (reqBodyBytes )
2417+ stopReq , err := http .NewRequest (http .MethodPost , fmt .Sprintf (stopEndpoint , endpointId , tc .faultType ), requestBody )
2418+ require .NoError (t , err )
2419+
2420+ ch2 := make (chan struct {
2421+ int
2422+ error
2423+ })
2424+
2425+ // Make an asynchronous Start request first
2426+ go makeAsyncRequest (router , startReq , ch1 )
2427+
2428+ // Waiting a bit before sending the stop request
2429+ time .Sleep (1 * time .Second )
2430+
2431+ // Make an asynchronous Stop request second
2432+ go makeAsyncRequest (router , stopReq , ch2 )
2433+
2434+ // Waiting to get the status code of the start request
2435+ resp1 := <- ch1
2436+ require .NoError (t , resp1 .error )
2437+ assert .Equal (t , http .StatusOK , resp1 .int )
2438+
2439+ // Waiting to get the status code of the stop request
2440+ resp2 := <- ch2
2441+ require .NoError (t , resp2 .error )
2442+ assert .Equal (t , http .StatusOK , resp2 .int )
2443+ })
2444+ }
2445+ }
2446+
2447+ // Helper function for making asynchronous mock HTTP requests
2448+ func makeAsyncRequest (router * mux.Router , req * http.Request , ch chan <- struct {
2449+ int
2450+ error
2451+ }) {
2452+ defer close (ch )
2453+
2454+ // Makes a mock HTTP request
2455+ recorder := httptest .NewRecorder ()
2456+ router .ServeHTTP (recorder , req )
2457+
2458+ var actualResponseBody types.NetworkFaultInjectionResponse
2459+ err := json .Unmarshal (recorder .Body .Bytes (), & actualResponseBody )
2460+ if err != nil {
2461+ ch <- struct {
2462+ int
2463+ error
2464+ }{- 1 , err }
2465+ } else {
2466+ ch <- struct {
2467+ int
2468+ error
2469+ }{recorder .Code , nil }
2470+ }
2471+ }
2472+
2473+ func convertToInterfaceList (strings []string ) []interface {} {
2474+ interfaces := make ([]interface {}, len (strings ))
2475+ for i , s := range strings {
2476+ interfaces [i ] = s
2477+ }
2478+ return interfaces
2479+ }
0 commit comments