github.com/outbrain/consul@v1.4.5/agent/checks/check_test.go (about)

     1  package checks
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"net"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"os"
    12  	"reflect"
    13  	"regexp"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/hashicorp/consul/agent/mock"
    19  	"github.com/hashicorp/consul/api"
    20  	"github.com/hashicorp/consul/testutil/retry"
    21  	"github.com/hashicorp/consul/types"
    22  	uuid "github.com/hashicorp/go-uuid"
    23  )
    24  
    25  func uniqueID() string {
    26  	id, err := uuid.GenerateUUID()
    27  	if err != nil {
    28  		panic(err)
    29  	}
    30  	return id
    31  }
    32  
    33  func TestCheckMonitor_Script(t *testing.T) {
    34  	tests := []struct {
    35  		script, status string
    36  	}{
    37  		{"exit 0", "passing"},
    38  		{"exit 1", "warning"},
    39  		{"exit 2", "critical"},
    40  		{"foobarbaz", "critical"},
    41  	}
    42  
    43  	for _, tt := range tests {
    44  		t.Run(tt.status, func(t *testing.T) {
    45  			notif := mock.NewNotify()
    46  			check := &CheckMonitor{
    47  				Notify:   notif,
    48  				CheckID:  types.CheckID("foo"),
    49  				Script:   tt.script,
    50  				Interval: 25 * time.Millisecond,
    51  				Logger:   log.New(ioutil.Discard, uniqueID(), log.LstdFlags),
    52  			}
    53  			check.Start()
    54  			defer check.Stop()
    55  			retry.Run(t, func(r *retry.R) {
    56  				if got, want := notif.Updates("foo"), 2; got < want {
    57  					r.Fatalf("got %d updates want at least %d", got, want)
    58  				}
    59  				if got, want := notif.State("foo"), tt.status; got != want {
    60  					r.Fatalf("got state %q want %q", got, want)
    61  				}
    62  			})
    63  		})
    64  	}
    65  }
    66  
    67  func TestCheckMonitor_Args(t *testing.T) {
    68  	tests := []struct {
    69  		args   []string
    70  		status string
    71  	}{
    72  		{[]string{"sh", "-c", "exit 0"}, "passing"},
    73  		{[]string{"sh", "-c", "exit 1"}, "warning"},
    74  		{[]string{"sh", "-c", "exit 2"}, "critical"},
    75  		{[]string{"foobarbaz"}, "critical"},
    76  	}
    77  
    78  	for _, tt := range tests {
    79  		t.Run(tt.status, func(t *testing.T) {
    80  			notif := mock.NewNotify()
    81  			check := &CheckMonitor{
    82  				Notify:     notif,
    83  				CheckID:    types.CheckID("foo"),
    84  				ScriptArgs: tt.args,
    85  				Interval:   25 * time.Millisecond,
    86  				Logger:     log.New(ioutil.Discard, uniqueID(), log.LstdFlags),
    87  			}
    88  			check.Start()
    89  			defer check.Stop()
    90  			retry.Run(t, func(r *retry.R) {
    91  				if got, want := notif.Updates("foo"), 2; got < want {
    92  					r.Fatalf("got %d updates want at least %d", got, want)
    93  				}
    94  				if got, want := notif.State("foo"), tt.status; got != want {
    95  					r.Fatalf("got state %q want %q", got, want)
    96  				}
    97  			})
    98  		})
    99  	}
   100  }
   101  
   102  func TestCheckMonitor_Timeout(t *testing.T) {
   103  	// t.Parallel() // timing test. no parallel
   104  	notif := mock.NewNotify()
   105  	check := &CheckMonitor{
   106  		Notify:     notif,
   107  		CheckID:    types.CheckID("foo"),
   108  		ScriptArgs: []string{"sh", "-c", "sleep 1 && exit 0"},
   109  		Interval:   50 * time.Millisecond,
   110  		Timeout:    25 * time.Millisecond,
   111  		Logger:     log.New(ioutil.Discard, uniqueID(), log.LstdFlags),
   112  	}
   113  	check.Start()
   114  	defer check.Stop()
   115  
   116  	time.Sleep(250 * time.Millisecond)
   117  
   118  	// Should have at least 2 updates
   119  	if notif.Updates("foo") < 2 {
   120  		t.Fatalf("should have at least 2 updates %v", notif.UpdatesMap())
   121  	}
   122  	if notif.State("foo") != "critical" {
   123  		t.Fatalf("should be critical %v", notif.StateMap())
   124  	}
   125  }
   126  
   127  func TestCheckMonitor_RandomStagger(t *testing.T) {
   128  	// t.Parallel() // timing test. no parallel
   129  	notif := mock.NewNotify()
   130  	check := &CheckMonitor{
   131  		Notify:     notif,
   132  		CheckID:    types.CheckID("foo"),
   133  		ScriptArgs: []string{"sh", "-c", "exit 0"},
   134  		Interval:   25 * time.Millisecond,
   135  		Logger:     log.New(ioutil.Discard, uniqueID(), log.LstdFlags),
   136  	}
   137  	check.Start()
   138  	defer check.Stop()
   139  
   140  	time.Sleep(500 * time.Millisecond)
   141  
   142  	// Should have at least 1 update
   143  	if notif.Updates("foo") < 1 {
   144  		t.Fatalf("should have 1 or more updates %v", notif.UpdatesMap())
   145  	}
   146  
   147  	if notif.State("foo") != api.HealthPassing {
   148  		t.Fatalf("should be %v %v", api.HealthPassing, notif.StateMap())
   149  	}
   150  }
   151  
   152  func TestCheckMonitor_LimitOutput(t *testing.T) {
   153  	t.Parallel()
   154  	notif := mock.NewNotify()
   155  	check := &CheckMonitor{
   156  		Notify:     notif,
   157  		CheckID:    types.CheckID("foo"),
   158  		ScriptArgs: []string{"od", "-N", "81920", "/dev/urandom"},
   159  		Interval:   25 * time.Millisecond,
   160  		Logger:     log.New(ioutil.Discard, uniqueID(), log.LstdFlags),
   161  	}
   162  	check.Start()
   163  	defer check.Stop()
   164  
   165  	time.Sleep(50 * time.Millisecond)
   166  
   167  	// Allow for extra bytes for the truncation message
   168  	if len(notif.Output("foo")) > BufSize+100 {
   169  		t.Fatalf("output size is too long")
   170  	}
   171  }
   172  
   173  func TestCheckTTL(t *testing.T) {
   174  	// t.Parallel() // timing test. no parallel
   175  	notif := mock.NewNotify()
   176  	check := &CheckTTL{
   177  		Notify:  notif,
   178  		CheckID: types.CheckID("foo"),
   179  		TTL:     200 * time.Millisecond,
   180  		Logger:  log.New(ioutil.Discard, uniqueID(), log.LstdFlags),
   181  	}
   182  	check.Start()
   183  	defer check.Stop()
   184  
   185  	time.Sleep(100 * time.Millisecond)
   186  	check.SetStatus(api.HealthPassing, "test-output")
   187  
   188  	if notif.Updates("foo") != 1 {
   189  		t.Fatalf("should have 1 updates %v", notif.UpdatesMap())
   190  	}
   191  
   192  	if notif.State("foo") != api.HealthPassing {
   193  		t.Fatalf("should be passing %v", notif.StateMap())
   194  	}
   195  
   196  	// Ensure we don't fail early
   197  	time.Sleep(150 * time.Millisecond)
   198  	if notif.Updates("foo") != 1 {
   199  		t.Fatalf("should have 1 updates %v", notif.UpdatesMap())
   200  	}
   201  
   202  	// Wait for the TTL to expire
   203  	time.Sleep(150 * time.Millisecond)
   204  
   205  	if notif.Updates("foo") != 2 {
   206  		t.Fatalf("should have 2 updates %v", notif.UpdatesMap())
   207  	}
   208  
   209  	if notif.State("foo") != api.HealthCritical {
   210  		t.Fatalf("should be critical %v", notif.StateMap())
   211  	}
   212  
   213  	if !strings.Contains(notif.Output("foo"), "test-output") {
   214  		t.Fatalf("should have retained output %v", notif.OutputMap())
   215  	}
   216  }
   217  
   218  func TestCheckHTTP(t *testing.T) {
   219  	t.Parallel()
   220  
   221  	tests := []struct {
   222  		desc   string
   223  		code   int
   224  		method string
   225  		header http.Header
   226  		status string
   227  	}{
   228  		// passing
   229  		{code: 200, status: api.HealthPassing},
   230  		{code: 201, status: api.HealthPassing},
   231  		{code: 250, status: api.HealthPassing},
   232  		{code: 299, status: api.HealthPassing},
   233  
   234  		// warning
   235  		{code: 429, status: api.HealthWarning},
   236  
   237  		// critical
   238  		{code: 150, status: api.HealthCritical},
   239  		{code: 199, status: api.HealthCritical},
   240  		{code: 300, status: api.HealthCritical},
   241  		{code: 400, status: api.HealthCritical},
   242  		{code: 500, status: api.HealthCritical},
   243  
   244  		// custom method
   245  		{desc: "custom method GET", code: 200, method: "GET", status: api.HealthPassing},
   246  		{desc: "custom method POST", code: 200, header: http.Header{"Content-Length": []string{"0"}}, method: "POST", status: api.HealthPassing},
   247  		{desc: "custom method abc", code: 200, method: "abc", status: api.HealthPassing},
   248  
   249  		// custom header
   250  		{desc: "custom header", code: 200, header: http.Header{"A": []string{"b", "c"}}, status: api.HealthPassing},
   251  		{desc: "host header", code: 200, header: http.Header{"Host": []string{"a"}}, status: api.HealthPassing},
   252  	}
   253  
   254  	for _, tt := range tests {
   255  		desc := tt.desc
   256  		if desc == "" {
   257  			desc = fmt.Sprintf("code %d -> status %s", tt.code, tt.status)
   258  		}
   259  		t.Run(desc, func(t *testing.T) {
   260  			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   261  				if tt.method != "" && tt.method != r.Method {
   262  					w.WriteHeader(999)
   263  					return
   264  				}
   265  
   266  				expectedHeader := http.Header{
   267  					"Accept":          []string{"text/plain, text/*, */*"},
   268  					"Accept-Encoding": []string{"gzip"},
   269  					"Connection":      []string{"close"},
   270  					"User-Agent":      []string{"Consul Health Check"},
   271  				}
   272  				for k, v := range tt.header {
   273  					expectedHeader[k] = v
   274  				}
   275  
   276  				// the Host header is in r.Host and not in the headers
   277  				host := expectedHeader.Get("Host")
   278  				if host != "" && host != r.Host {
   279  					w.WriteHeader(999)
   280  					return
   281  				}
   282  				expectedHeader.Del("Host")
   283  
   284  				if !reflect.DeepEqual(expectedHeader, r.Header) {
   285  					w.WriteHeader(999)
   286  					return
   287  				}
   288  
   289  				// Body larger than 4k limit
   290  				body := bytes.Repeat([]byte{'a'}, 2*BufSize)
   291  				w.WriteHeader(tt.code)
   292  				w.Write(body)
   293  			}))
   294  			defer server.Close()
   295  
   296  			notif := mock.NewNotify()
   297  			check := &CheckHTTP{
   298  				Notify:   notif,
   299  				CheckID:  types.CheckID("foo"),
   300  				HTTP:     server.URL,
   301  				Method:   tt.method,
   302  				Header:   tt.header,
   303  				Interval: 10 * time.Millisecond,
   304  				Logger:   log.New(ioutil.Discard, uniqueID(), log.LstdFlags),
   305  			}
   306  			check.Start()
   307  			defer check.Stop()
   308  
   309  			retry.Run(t, func(r *retry.R) {
   310  				if got, want := notif.Updates("foo"), 2; got < want {
   311  					r.Fatalf("got %d updates want at least %d", got, want)
   312  				}
   313  				if got, want := notif.State("foo"), tt.status; got != want {
   314  					r.Fatalf("got state %q want %q", got, want)
   315  				}
   316  				// Allow slightly more data than BufSize, for the header
   317  				if n := len(notif.Output("foo")); n > (BufSize + 256) {
   318  					r.Fatalf("output too long: %d (%d-byte limit)", n, BufSize)
   319  				}
   320  			})
   321  		})
   322  	}
   323  }
   324  
   325  func TestCheckHTTPTimeout(t *testing.T) {
   326  	t.Parallel()
   327  	timeout := 5 * time.Millisecond
   328  	server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
   329  		time.Sleep(2 * timeout)
   330  	}))
   331  	defer server.Close()
   332  
   333  	notif := mock.NewNotify()
   334  	check := &CheckHTTP{
   335  		Notify:   notif,
   336  		CheckID:  types.CheckID("bar"),
   337  		HTTP:     server.URL,
   338  		Timeout:  timeout,
   339  		Interval: 10 * time.Millisecond,
   340  		Logger:   log.New(ioutil.Discard, uniqueID(), log.LstdFlags),
   341  	}
   342  
   343  	check.Start()
   344  	defer check.Stop()
   345  	retry.Run(t, func(r *retry.R) {
   346  		if got, want := notif.Updates("bar"), 2; got < want {
   347  			r.Fatalf("got %d updates want at least %d", got, want)
   348  		}
   349  		if got, want := notif.State("bar"), api.HealthCritical; got != want {
   350  			r.Fatalf("got state %q want %q", got, want)
   351  		}
   352  	})
   353  }
   354  
   355  func TestCheckHTTP_disablesKeepAlives(t *testing.T) {
   356  	t.Parallel()
   357  	check := &CheckHTTP{
   358  		CheckID:  types.CheckID("foo"),
   359  		HTTP:     "http://foo.bar/baz",
   360  		Interval: 10 * time.Second,
   361  		Logger:   log.New(ioutil.Discard, uniqueID(), log.LstdFlags),
   362  	}
   363  
   364  	check.Start()
   365  	defer check.Stop()
   366  
   367  	if !check.httpClient.Transport.(*http.Transport).DisableKeepAlives {
   368  		t.Fatalf("should have disabled keepalives")
   369  	}
   370  }
   371  
   372  func largeBodyHandler(code int) http.Handler {
   373  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   374  		// Body larger than 4k limit
   375  		body := bytes.Repeat([]byte{'a'}, 2*BufSize)
   376  		w.WriteHeader(code)
   377  		w.Write(body)
   378  	})
   379  }
   380  
   381  func TestCheckHTTP_TLS_SkipVerify(t *testing.T) {
   382  	t.Parallel()
   383  	server := httptest.NewTLSServer(largeBodyHandler(200))
   384  	defer server.Close()
   385  
   386  	tlsConfig := &api.TLSConfig{
   387  		InsecureSkipVerify: true,
   388  	}
   389  	tlsClientConfig, err := api.SetupTLSConfig(tlsConfig)
   390  	if err != nil {
   391  		t.Fatalf("err: %v", err)
   392  	}
   393  
   394  	notif := mock.NewNotify()
   395  	check := &CheckHTTP{
   396  		Notify:          notif,
   397  		CheckID:         types.CheckID("skipverify_true"),
   398  		HTTP:            server.URL,
   399  		Interval:        25 * time.Millisecond,
   400  		Logger:          log.New(ioutil.Discard, uniqueID(), log.LstdFlags),
   401  		TLSClientConfig: tlsClientConfig,
   402  	}
   403  
   404  	check.Start()
   405  	defer check.Stop()
   406  
   407  	if !check.httpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify {
   408  		t.Fatalf("should be true")
   409  	}
   410  
   411  	retry.Run(t, func(r *retry.R) {
   412  		if got, want := notif.State("skipverify_true"), api.HealthPassing; got != want {
   413  			r.Fatalf("got state %q want %q", got, want)
   414  		}
   415  	})
   416  }
   417  
   418  func TestCheckHTTP_TLS_BadVerify(t *testing.T) {
   419  	t.Parallel()
   420  	server := httptest.NewTLSServer(largeBodyHandler(200))
   421  	defer server.Close()
   422  
   423  	tlsClientConfig, err := api.SetupTLSConfig(&api.TLSConfig{})
   424  	if err != nil {
   425  		t.Fatalf("err: %v", err)
   426  	}
   427  
   428  	notif := mock.NewNotify()
   429  	check := &CheckHTTP{
   430  		Notify:          notif,
   431  		CheckID:         types.CheckID("skipverify_false"),
   432  		HTTP:            server.URL,
   433  		Interval:        100 * time.Millisecond,
   434  		Logger:          log.New(ioutil.Discard, uniqueID(), log.LstdFlags),
   435  		TLSClientConfig: tlsClientConfig,
   436  	}
   437  
   438  	check.Start()
   439  	defer check.Stop()
   440  
   441  	if check.httpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify {
   442  		t.Fatalf("should default to false")
   443  	}
   444  
   445  	retry.Run(t, func(r *retry.R) {
   446  		// This should fail due to an invalid SSL cert
   447  		if got, want := notif.State("skipverify_false"), api.HealthCritical; got != want {
   448  			r.Fatalf("got state %q want %q", got, want)
   449  		}
   450  		if !strings.Contains(notif.Output("skipverify_false"), "certificate signed by unknown authority") {
   451  			r.Fatalf("should fail with certificate error %v", notif.OutputMap())
   452  		}
   453  	})
   454  }
   455  
   456  func mockTCPServer(network string) net.Listener {
   457  	var (
   458  		addr string
   459  	)
   460  
   461  	if network == `tcp6` {
   462  		addr = `[::1]:0`
   463  	} else {
   464  		addr = `127.0.0.1:0`
   465  	}
   466  
   467  	listener, err := net.Listen(network, addr)
   468  	if err != nil {
   469  		panic(err)
   470  	}
   471  
   472  	return listener
   473  }
   474  
   475  func expectTCPStatus(t *testing.T, tcp string, status string) {
   476  	notif := mock.NewNotify()
   477  	check := &CheckTCP{
   478  		Notify:   notif,
   479  		CheckID:  types.CheckID("foo"),
   480  		TCP:      tcp,
   481  		Interval: 10 * time.Millisecond,
   482  		Logger:   log.New(ioutil.Discard, uniqueID(), log.LstdFlags),
   483  	}
   484  	check.Start()
   485  	defer check.Stop()
   486  	retry.Run(t, func(r *retry.R) {
   487  		if got, want := notif.Updates("foo"), 2; got < want {
   488  			r.Fatalf("got %d updates want at least %d", got, want)
   489  		}
   490  		if got, want := notif.State("foo"), status; got != want {
   491  			r.Fatalf("got state %q want %q", got, want)
   492  		}
   493  	})
   494  }
   495  
   496  func TestCheckTCPCritical(t *testing.T) {
   497  	t.Parallel()
   498  	var (
   499  		tcpServer net.Listener
   500  	)
   501  
   502  	tcpServer = mockTCPServer(`tcp`)
   503  	expectTCPStatus(t, `127.0.0.1:0`, api.HealthCritical)
   504  	tcpServer.Close()
   505  }
   506  
   507  func TestCheckTCPPassing(t *testing.T) {
   508  	t.Parallel()
   509  	var (
   510  		tcpServer net.Listener
   511  	)
   512  
   513  	tcpServer = mockTCPServer(`tcp`)
   514  	expectTCPStatus(t, tcpServer.Addr().String(), api.HealthPassing)
   515  	tcpServer.Close()
   516  
   517  	if os.Getenv("TRAVIS") == "true" {
   518  		t.Skip("IPV6 not supported on travis-ci")
   519  	}
   520  	tcpServer = mockTCPServer(`tcp6`)
   521  	expectTCPStatus(t, tcpServer.Addr().String(), api.HealthPassing)
   522  	tcpServer.Close()
   523  }
   524  
   525  func TestCheck_Docker(t *testing.T) {
   526  	tests := []struct {
   527  		desc     string
   528  		handlers map[string]http.HandlerFunc
   529  		out      *regexp.Regexp
   530  		state    string
   531  	}{
   532  		{
   533  			desc: "create exec: bad container id",
   534  			handlers: map[string]http.HandlerFunc{
   535  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   536  					w.WriteHeader(404)
   537  				},
   538  			},
   539  			out:   regexp.MustCompile("^create exec failed for unknown container 123$"),
   540  			state: api.HealthCritical,
   541  		},
   542  		{
   543  			desc: "create exec: paused container",
   544  			handlers: map[string]http.HandlerFunc{
   545  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   546  					w.WriteHeader(409)
   547  				},
   548  			},
   549  			out:   regexp.MustCompile("^create exec failed since container 123 is paused or stopped$"),
   550  			state: api.HealthCritical,
   551  		},
   552  		{
   553  			desc: "create exec: bad status code",
   554  			handlers: map[string]http.HandlerFunc{
   555  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   556  					w.WriteHeader(999)
   557  					fmt.Fprint(w, "some output")
   558  				},
   559  			},
   560  			out:   regexp.MustCompile("^create exec failed for container 123 with status 999: some output$"),
   561  			state: api.HealthCritical,
   562  		},
   563  		{
   564  			desc: "create exec: bad json",
   565  			handlers: map[string]http.HandlerFunc{
   566  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   567  					w.WriteHeader(201)
   568  					w.Header().Set("Content-Type", "application/json")
   569  					fmt.Fprint(w, `this is not json`)
   570  				},
   571  			},
   572  			out:   regexp.MustCompile("^create exec response for container 123 cannot be parsed: .*$"),
   573  			state: api.HealthCritical,
   574  		},
   575  		{
   576  			desc: "start exec: bad exec id",
   577  			handlers: map[string]http.HandlerFunc{
   578  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   579  					w.WriteHeader(201)
   580  					w.Header().Set("Content-Type", "application/json")
   581  					fmt.Fprint(w, `{"Id":"456"}`)
   582  				},
   583  				"POST /exec/456/start": func(w http.ResponseWriter, r *http.Request) {
   584  					w.WriteHeader(404)
   585  				},
   586  			},
   587  			out:   regexp.MustCompile("^start exec failed for container 123: invalid exec id 456$"),
   588  			state: api.HealthCritical,
   589  		},
   590  		{
   591  			desc: "start exec: paused container",
   592  			handlers: map[string]http.HandlerFunc{
   593  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   594  					w.WriteHeader(201)
   595  					w.Header().Set("Content-Type", "application/json")
   596  					fmt.Fprint(w, `{"Id":"456"}`)
   597  				},
   598  				"POST /exec/456/start": func(w http.ResponseWriter, r *http.Request) {
   599  					w.WriteHeader(409)
   600  				},
   601  			},
   602  			out:   regexp.MustCompile("^start exec failed since container 123 is paused or stopped$"),
   603  			state: api.HealthCritical,
   604  		},
   605  		{
   606  			desc: "start exec: bad status code",
   607  			handlers: map[string]http.HandlerFunc{
   608  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   609  					w.WriteHeader(201)
   610  					w.Header().Set("Content-Type", "application/json")
   611  					fmt.Fprint(w, `{"Id":"456"}`)
   612  				},
   613  				"POST /exec/456/start": func(w http.ResponseWriter, r *http.Request) {
   614  					w.WriteHeader(999)
   615  					fmt.Fprint(w, "some output")
   616  				},
   617  			},
   618  			out:   regexp.MustCompile("^start exec failed for container 123 with status 999: body: some output err: <nil>$"),
   619  			state: api.HealthCritical,
   620  		},
   621  		{
   622  			desc: "inspect exec: bad exec id",
   623  			handlers: map[string]http.HandlerFunc{
   624  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   625  					w.WriteHeader(201)
   626  					w.Header().Set("Content-Type", "application/json")
   627  					fmt.Fprint(w, `{"Id":"456"}`)
   628  				},
   629  				"POST /exec/456/start": func(w http.ResponseWriter, r *http.Request) {
   630  					w.WriteHeader(200)
   631  					fmt.Fprint(w, "OK")
   632  				},
   633  				"GET /exec/456/json": func(w http.ResponseWriter, r *http.Request) {
   634  					w.WriteHeader(404)
   635  				},
   636  			},
   637  			out:   regexp.MustCompile("^inspect exec failed for container 123: invalid exec id 456$"),
   638  			state: api.HealthCritical,
   639  		},
   640  		{
   641  			desc: "inspect exec: bad status code",
   642  			handlers: map[string]http.HandlerFunc{
   643  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   644  					w.WriteHeader(201)
   645  					w.Header().Set("Content-Type", "application/json")
   646  					fmt.Fprint(w, `{"Id":"456"}`)
   647  				},
   648  				"POST /exec/456/start": func(w http.ResponseWriter, r *http.Request) {
   649  					w.WriteHeader(200)
   650  					fmt.Fprint(w, "OK")
   651  				},
   652  				"GET /exec/456/json": func(w http.ResponseWriter, r *http.Request) {
   653  					w.WriteHeader(999)
   654  					fmt.Fprint(w, "some output")
   655  				},
   656  			},
   657  			out:   regexp.MustCompile("^inspect exec failed for container 123 with status 999: some output$"),
   658  			state: api.HealthCritical,
   659  		},
   660  		{
   661  			desc: "inspect exec: bad json",
   662  			handlers: map[string]http.HandlerFunc{
   663  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   664  					w.WriteHeader(201)
   665  					w.Header().Set("Content-Type", "application/json")
   666  					fmt.Fprint(w, `{"Id":"456"}`)
   667  				},
   668  				"POST /exec/456/start": func(w http.ResponseWriter, r *http.Request) {
   669  					w.WriteHeader(200)
   670  					fmt.Fprint(w, "OK")
   671  				},
   672  				"GET /exec/456/json": func(w http.ResponseWriter, r *http.Request) {
   673  					w.WriteHeader(200)
   674  					w.Header().Set("Content-Type", "application/json")
   675  					fmt.Fprint(w, `this is not json`)
   676  				},
   677  			},
   678  			out:   regexp.MustCompile("^inspect exec response for container 123 cannot be parsed: .*$"),
   679  			state: api.HealthCritical,
   680  		},
   681  		{
   682  			desc: "inspect exec: exit code 0: passing",
   683  			handlers: map[string]http.HandlerFunc{
   684  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   685  					w.WriteHeader(201)
   686  					w.Header().Set("Content-Type", "application/json")
   687  					fmt.Fprint(w, `{"Id":"456"}`)
   688  				},
   689  				"POST /exec/456/start": func(w http.ResponseWriter, r *http.Request) {
   690  					w.WriteHeader(200)
   691  					fmt.Fprint(w, "OK")
   692  				},
   693  				"GET /exec/456/json": func(w http.ResponseWriter, r *http.Request) {
   694  					w.WriteHeader(200)
   695  					w.Header().Set("Content-Type", "application/json")
   696  					fmt.Fprint(w, `{"ExitCode":0}`)
   697  				},
   698  			},
   699  			out:   regexp.MustCompile("^OK$"),
   700  			state: api.HealthPassing,
   701  		},
   702  		{
   703  			desc: "inspect exec: exit code 0: passing: truncated",
   704  			handlers: map[string]http.HandlerFunc{
   705  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   706  					w.WriteHeader(201)
   707  					w.Header().Set("Content-Type", "application/json")
   708  					fmt.Fprint(w, `{"Id":"456"}`)
   709  				},
   710  				"POST /exec/456/start": func(w http.ResponseWriter, r *http.Request) {
   711  					w.WriteHeader(200)
   712  					fmt.Fprint(w, "01234567890123456789OK") // more than 20 bytes
   713  				},
   714  				"GET /exec/456/json": func(w http.ResponseWriter, r *http.Request) {
   715  					w.WriteHeader(200)
   716  					w.Header().Set("Content-Type", "application/json")
   717  					fmt.Fprint(w, `{"ExitCode":0}`)
   718  				},
   719  			},
   720  			out:   regexp.MustCompile("^Captured 20 of 22 bytes\n...\n234567890123456789OK$"),
   721  			state: api.HealthPassing,
   722  		},
   723  		{
   724  			desc: "inspect exec: exit code 1: warning",
   725  			handlers: map[string]http.HandlerFunc{
   726  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   727  					w.WriteHeader(201)
   728  					w.Header().Set("Content-Type", "application/json")
   729  					fmt.Fprint(w, `{"Id":"456"}`)
   730  				},
   731  				"POST /exec/456/start": func(w http.ResponseWriter, r *http.Request) {
   732  					w.WriteHeader(200)
   733  					fmt.Fprint(w, "WARN")
   734  				},
   735  				"GET /exec/456/json": func(w http.ResponseWriter, r *http.Request) {
   736  					w.WriteHeader(200)
   737  					w.Header().Set("Content-Type", "application/json")
   738  					fmt.Fprint(w, `{"ExitCode":1}`)
   739  				},
   740  			},
   741  			out:   regexp.MustCompile("^WARN$"),
   742  			state: api.HealthWarning,
   743  		},
   744  		{
   745  			desc: "inspect exec: exit code 2: critical",
   746  			handlers: map[string]http.HandlerFunc{
   747  				"POST /containers/123/exec": func(w http.ResponseWriter, r *http.Request) {
   748  					w.WriteHeader(201)
   749  					w.Header().Set("Content-Type", "application/json")
   750  					fmt.Fprint(w, `{"Id":"456"}`)
   751  				},
   752  				"POST /exec/456/start": func(w http.ResponseWriter, r *http.Request) {
   753  					w.WriteHeader(200)
   754  					fmt.Fprint(w, "NOK")
   755  				},
   756  				"GET /exec/456/json": func(w http.ResponseWriter, r *http.Request) {
   757  					w.WriteHeader(200)
   758  					w.Header().Set("Content-Type", "application/json")
   759  					fmt.Fprint(w, `{"ExitCode":2}`)
   760  				},
   761  			},
   762  			out:   regexp.MustCompile("^NOK$"),
   763  			state: api.HealthCritical,
   764  		},
   765  	}
   766  
   767  	for _, tt := range tests {
   768  		t.Run(tt.desc, func(t *testing.T) {
   769  			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   770  				x := r.Method + " " + r.RequestURI
   771  				h := tt.handlers[x]
   772  				if h == nil {
   773  					t.Fatalf("bad url %s", x)
   774  				}
   775  				h(w, r)
   776  			}))
   777  			defer srv.Close()
   778  
   779  			// create a docker client with a tiny output buffer
   780  			// to test the truncation
   781  			c, err := NewDockerClient(srv.URL, 20)
   782  			if err != nil {
   783  				t.Fatal(err)
   784  			}
   785  
   786  			notif, upd := mock.NewNotifyChan()
   787  			id := types.CheckID("chk")
   788  			check := &CheckDocker{
   789  				Notify:            notif,
   790  				CheckID:           id,
   791  				ScriptArgs:        []string{"/health.sh"},
   792  				DockerContainerID: "123",
   793  				Interval:          25 * time.Millisecond,
   794  				Client:            c,
   795  			}
   796  			check.Start()
   797  			defer check.Stop()
   798  
   799  			<-upd // wait for update
   800  
   801  			if got, want := notif.Output(id), tt.out; !want.MatchString(got) {
   802  				t.Fatalf("got %q want %q", got, want)
   803  			}
   804  			if got, want := notif.State(id), tt.state; got != want {
   805  				t.Fatalf("got status %q want %q", got, want)
   806  			}
   807  		})
   808  	}
   809  }