github.com/jrxfive/nomad@v0.6.1-0.20170802162750-1fef470e89bf/command/agent/http_test.go (about)

     1  package agent
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/tls"
     6  	"crypto/x509"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"net"
    12  	"net/http"
    13  	"net/http/httptest"
    14  	"net/url"
    15  	"strconv"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/hashicorp/nomad/nomad/mock"
    20  	"github.com/hashicorp/nomad/nomad/structs"
    21  	"github.com/hashicorp/nomad/nomad/structs/config"
    22  	"github.com/hashicorp/nomad/testutil"
    23  )
    24  
    25  // makeHTTPServer returns a test server whose logs will be written to
    26  // the passed writer. If the writer is nil, the logs are written to stderr.
    27  func makeHTTPServer(t testing.TB, cb func(c *Config)) *TestAgent {
    28  	return NewTestAgent(t.Name(), cb)
    29  }
    30  
    31  func BenchmarkHTTPRequests(b *testing.B) {
    32  	s := makeHTTPServer(b, func(c *Config) {
    33  		c.Client.Enabled = false
    34  	})
    35  	defer s.Shutdown()
    36  
    37  	job := mock.Job()
    38  	var allocs []*structs.Allocation
    39  	count := 1000
    40  	for i := 0; i < count; i++ {
    41  		alloc := mock.Alloc()
    42  		alloc.Job = job
    43  		alloc.JobID = job.ID
    44  		alloc.Name = fmt.Sprintf("my-job.web[%d]", i)
    45  		allocs = append(allocs, alloc)
    46  	}
    47  
    48  	handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    49  		return allocs[:count], nil
    50  	}
    51  	b.ResetTimer()
    52  
    53  	b.RunParallel(func(pb *testing.PB) {
    54  		for pb.Next() {
    55  			resp := httptest.NewRecorder()
    56  			req, _ := http.NewRequest("GET", "/v1/kv/key", nil)
    57  			s.Server.wrap(handler)(resp, req)
    58  		}
    59  	})
    60  }
    61  
    62  func TestSetIndex(t *testing.T) {
    63  	t.Parallel()
    64  	resp := httptest.NewRecorder()
    65  	setIndex(resp, 1000)
    66  	header := resp.Header().Get("X-Nomad-Index")
    67  	if header != "1000" {
    68  		t.Fatalf("Bad: %v", header)
    69  	}
    70  	setIndex(resp, 2000)
    71  	if v := resp.Header()["X-Nomad-Index"]; len(v) != 1 {
    72  		t.Fatalf("bad: %#v", v)
    73  	}
    74  }
    75  
    76  func TestSetKnownLeader(t *testing.T) {
    77  	t.Parallel()
    78  	resp := httptest.NewRecorder()
    79  	setKnownLeader(resp, true)
    80  	header := resp.Header().Get("X-Nomad-KnownLeader")
    81  	if header != "true" {
    82  		t.Fatalf("Bad: %v", header)
    83  	}
    84  	resp = httptest.NewRecorder()
    85  	setKnownLeader(resp, false)
    86  	header = resp.Header().Get("X-Nomad-KnownLeader")
    87  	if header != "false" {
    88  		t.Fatalf("Bad: %v", header)
    89  	}
    90  }
    91  
    92  func TestSetLastContact(t *testing.T) {
    93  	t.Parallel()
    94  	resp := httptest.NewRecorder()
    95  	setLastContact(resp, 123456*time.Microsecond)
    96  	header := resp.Header().Get("X-Nomad-LastContact")
    97  	if header != "123" {
    98  		t.Fatalf("Bad: %v", header)
    99  	}
   100  }
   101  
   102  func TestSetMeta(t *testing.T) {
   103  	t.Parallel()
   104  	meta := structs.QueryMeta{
   105  		Index:       1000,
   106  		KnownLeader: true,
   107  		LastContact: 123456 * time.Microsecond,
   108  	}
   109  	resp := httptest.NewRecorder()
   110  	setMeta(resp, &meta)
   111  	header := resp.Header().Get("X-Nomad-Index")
   112  	if header != "1000" {
   113  		t.Fatalf("Bad: %v", header)
   114  	}
   115  	header = resp.Header().Get("X-Nomad-KnownLeader")
   116  	if header != "true" {
   117  		t.Fatalf("Bad: %v", header)
   118  	}
   119  	header = resp.Header().Get("X-Nomad-LastContact")
   120  	if header != "123" {
   121  		t.Fatalf("Bad: %v", header)
   122  	}
   123  }
   124  
   125  func TestSetHeaders(t *testing.T) {
   126  	t.Parallel()
   127  	s := makeHTTPServer(t, nil)
   128  	s.Agent.config.HTTPAPIResponseHeaders = map[string]string{"foo": "bar"}
   129  	defer s.Shutdown()
   130  
   131  	resp := httptest.NewRecorder()
   132  	handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   133  		return &structs.Job{Name: "foo"}, nil
   134  	}
   135  
   136  	req, _ := http.NewRequest("GET", "/v1/kv/key", nil)
   137  	s.Server.wrap(handler)(resp, req)
   138  	header := resp.Header().Get("foo")
   139  
   140  	if header != "bar" {
   141  		t.Fatalf("expected header: %v, actual: %v", "bar", header)
   142  	}
   143  
   144  }
   145  
   146  func TestContentTypeIsJSON(t *testing.T) {
   147  	t.Parallel()
   148  	s := makeHTTPServer(t, nil)
   149  	defer s.Shutdown()
   150  
   151  	resp := httptest.NewRecorder()
   152  
   153  	handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   154  		return &structs.Job{Name: "foo"}, nil
   155  	}
   156  
   157  	req, _ := http.NewRequest("GET", "/v1/kv/key", nil)
   158  	s.Server.wrap(handler)(resp, req)
   159  
   160  	contentType := resp.Header().Get("Content-Type")
   161  
   162  	if contentType != "application/json" {
   163  		t.Fatalf("Content-Type header was not 'application/json'")
   164  	}
   165  }
   166  
   167  func TestPrettyPrint(t *testing.T) {
   168  	t.Parallel()
   169  	testPrettyPrint("pretty=1", true, t)
   170  }
   171  
   172  func TestPrettyPrintOff(t *testing.T) {
   173  	t.Parallel()
   174  	testPrettyPrint("pretty=0", false, t)
   175  }
   176  
   177  func TestPrettyPrintBare(t *testing.T) {
   178  	t.Parallel()
   179  	testPrettyPrint("pretty", true, t)
   180  }
   181  
   182  func testPrettyPrint(pretty string, prettyFmt bool, t *testing.T) {
   183  	s := makeHTTPServer(t, nil)
   184  	defer s.Shutdown()
   185  
   186  	r := &structs.Job{Name: "foo"}
   187  
   188  	resp := httptest.NewRecorder()
   189  	handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   190  		return r, nil
   191  	}
   192  
   193  	urlStr := "/v1/job/foo?" + pretty
   194  	req, _ := http.NewRequest("GET", urlStr, nil)
   195  	s.Server.wrap(handler)(resp, req)
   196  
   197  	var expected []byte
   198  	if prettyFmt {
   199  		expected, _ = json.MarshalIndent(r, "", "    ")
   200  		expected = append(expected, "\n"...)
   201  	} else {
   202  		expected, _ = json.Marshal(r)
   203  	}
   204  	actual, err := ioutil.ReadAll(resp.Body)
   205  	if err != nil {
   206  		t.Fatalf("err: %s", err)
   207  	}
   208  
   209  	if !bytes.Equal(expected, actual) {
   210  		t.Fatalf("bad:\nexpected:\t%q\nactual:\t\t%q", string(expected), string(actual))
   211  	}
   212  }
   213  
   214  func TestParseWait(t *testing.T) {
   215  	t.Parallel()
   216  	resp := httptest.NewRecorder()
   217  	var b structs.QueryOptions
   218  
   219  	req, err := http.NewRequest("GET",
   220  		"/v1/catalog/nodes?wait=60s&index=1000", nil)
   221  	if err != nil {
   222  		t.Fatalf("err: %v", err)
   223  	}
   224  
   225  	if d := parseWait(resp, req, &b); d {
   226  		t.Fatalf("unexpected done")
   227  	}
   228  
   229  	if b.MinQueryIndex != 1000 {
   230  		t.Fatalf("Bad: %v", b)
   231  	}
   232  	if b.MaxQueryTime != 60*time.Second {
   233  		t.Fatalf("Bad: %v", b)
   234  	}
   235  }
   236  
   237  func TestParseWait_InvalidTime(t *testing.T) {
   238  	t.Parallel()
   239  	resp := httptest.NewRecorder()
   240  	var b structs.QueryOptions
   241  
   242  	req, err := http.NewRequest("GET",
   243  		"/v1/catalog/nodes?wait=60foo&index=1000", nil)
   244  	if err != nil {
   245  		t.Fatalf("err: %v", err)
   246  	}
   247  
   248  	if d := parseWait(resp, req, &b); !d {
   249  		t.Fatalf("expected done")
   250  	}
   251  
   252  	if resp.Code != 400 {
   253  		t.Fatalf("bad code: %v", resp.Code)
   254  	}
   255  }
   256  
   257  func TestParseWait_InvalidIndex(t *testing.T) {
   258  	t.Parallel()
   259  	resp := httptest.NewRecorder()
   260  	var b structs.QueryOptions
   261  
   262  	req, err := http.NewRequest("GET",
   263  		"/v1/catalog/nodes?wait=60s&index=foo", nil)
   264  	if err != nil {
   265  		t.Fatalf("err: %v", err)
   266  	}
   267  
   268  	if d := parseWait(resp, req, &b); !d {
   269  		t.Fatalf("expected done")
   270  	}
   271  
   272  	if resp.Code != 400 {
   273  		t.Fatalf("bad code: %v", resp.Code)
   274  	}
   275  }
   276  
   277  func TestParseConsistency(t *testing.T) {
   278  	t.Parallel()
   279  	var b structs.QueryOptions
   280  
   281  	req, err := http.NewRequest("GET",
   282  		"/v1/catalog/nodes?stale", nil)
   283  	if err != nil {
   284  		t.Fatalf("err: %v", err)
   285  	}
   286  
   287  	parseConsistency(req, &b)
   288  	if !b.AllowStale {
   289  		t.Fatalf("Bad: %v", b)
   290  	}
   291  
   292  	b = structs.QueryOptions{}
   293  	req, err = http.NewRequest("GET",
   294  		"/v1/catalog/nodes?consistent", nil)
   295  	if err != nil {
   296  		t.Fatalf("err: %v", err)
   297  	}
   298  
   299  	parseConsistency(req, &b)
   300  	if b.AllowStale {
   301  		t.Fatalf("Bad: %v", b)
   302  	}
   303  }
   304  
   305  func TestParseRegion(t *testing.T) {
   306  	t.Parallel()
   307  	s := makeHTTPServer(t, nil)
   308  	defer s.Shutdown()
   309  
   310  	req, err := http.NewRequest("GET",
   311  		"/v1/jobs?region=foo", nil)
   312  	if err != nil {
   313  		t.Fatalf("err: %v", err)
   314  	}
   315  
   316  	var region string
   317  	s.Server.parseRegion(req, &region)
   318  	if region != "foo" {
   319  		t.Fatalf("bad %s", region)
   320  	}
   321  
   322  	region = ""
   323  	req, err = http.NewRequest("GET", "/v1/jobs", nil)
   324  	if err != nil {
   325  		t.Fatalf("err: %v", err)
   326  	}
   327  
   328  	s.Server.parseRegion(req, &region)
   329  	if region != "global" {
   330  		t.Fatalf("bad %s", region)
   331  	}
   332  }
   333  
   334  // TestHTTP_VerifyHTTPSClient asserts that a client certificate signed by the
   335  // appropriate CA is required when VerifyHTTPSClient=true.
   336  func TestHTTP_VerifyHTTPSClient(t *testing.T) {
   337  	t.Parallel()
   338  	const (
   339  		cafile  = "../../helper/tlsutil/testdata/ca.pem"
   340  		foocert = "../../helper/tlsutil/testdata/nomad-foo.pem"
   341  		fookey  = "../../helper/tlsutil/testdata/nomad-foo-key.pem"
   342  	)
   343  	s := makeHTTPServer(t, func(c *Config) {
   344  		c.Region = "foo" // match the region on foocert
   345  		c.TLSConfig = &config.TLSConfig{
   346  			EnableHTTP:        true,
   347  			VerifyHTTPSClient: true,
   348  			CAFile:            cafile,
   349  			CertFile:          foocert,
   350  			KeyFile:           fookey,
   351  		}
   352  	})
   353  	defer s.Shutdown()
   354  
   355  	reqURL := fmt.Sprintf("https://%s/v1/agent/self", s.Agent.config.AdvertiseAddrs.HTTP)
   356  
   357  	// FAIL: Requests that expect 127.0.0.1 as the name should fail
   358  	resp, err := http.Get(reqURL)
   359  	if err == nil {
   360  		resp.Body.Close()
   361  		t.Fatalf("expected non-nil error but received: %v", resp.StatusCode)
   362  	}
   363  	urlErr, ok := err.(*url.Error)
   364  	if !ok {
   365  		t.Fatalf("expected a *url.Error but received: %T -> %v", err, err)
   366  	}
   367  	hostErr, ok := urlErr.Err.(x509.HostnameError)
   368  	if !ok {
   369  		t.Fatalf("expected a x509.HostnameError but received: %T -> %v", urlErr.Err, urlErr.Err)
   370  	}
   371  	if expected := "127.0.0.1"; hostErr.Host != expected {
   372  		t.Fatalf("expected hostname on error to be %q but found %q", expected, hostErr.Host)
   373  	}
   374  
   375  	// FAIL: Requests that specify a valid hostname but not the CA should
   376  	// fail
   377  	tlsConf := &tls.Config{
   378  		ServerName: "client.regionFoo.nomad",
   379  	}
   380  	transport := &http.Transport{TLSClientConfig: tlsConf}
   381  	client := &http.Client{Transport: transport}
   382  	req, err := http.NewRequest("GET", reqURL, nil)
   383  	if err != nil {
   384  		t.Fatalf("error creating request: %v", err)
   385  	}
   386  	resp, err = client.Do(req)
   387  	if err == nil {
   388  		resp.Body.Close()
   389  		t.Fatalf("expected non-nil error but received: %v", resp.StatusCode)
   390  	}
   391  	urlErr, ok = err.(*url.Error)
   392  	if !ok {
   393  		t.Fatalf("expected a *url.Error but received: %T -> %v", err, err)
   394  	}
   395  	_, ok = urlErr.Err.(x509.UnknownAuthorityError)
   396  	if !ok {
   397  		t.Fatalf("expected a x509.UnknownAuthorityError but received: %T -> %v", urlErr.Err, urlErr.Err)
   398  	}
   399  
   400  	// FAIL: Requests that specify a valid hostname and CA cert but lack a
   401  	// client certificate should fail
   402  	cacertBytes, err := ioutil.ReadFile(cafile)
   403  	if err != nil {
   404  		t.Fatalf("error reading cacert: %v", err)
   405  	}
   406  	tlsConf.RootCAs = x509.NewCertPool()
   407  	tlsConf.RootCAs.AppendCertsFromPEM(cacertBytes)
   408  	req, err = http.NewRequest("GET", reqURL, nil)
   409  	if err != nil {
   410  		t.Fatalf("error creating request: %v", err)
   411  	}
   412  	resp, err = client.Do(req)
   413  	if err == nil {
   414  		resp.Body.Close()
   415  		t.Fatalf("expected non-nil error but received: %v", resp.StatusCode)
   416  	}
   417  	urlErr, ok = err.(*url.Error)
   418  	if !ok {
   419  		t.Fatalf("expected a *url.Error but received: %T -> %v", err, err)
   420  	}
   421  	opErr, ok := urlErr.Err.(*net.OpError)
   422  	if !ok {
   423  		t.Fatalf("expected a *net.OpErr but received: %T -> %v", urlErr.Err, urlErr.Err)
   424  	}
   425  	const badCertificate = "tls: bad certificate" // from crypto/tls/alert.go:52 and RFC 5246 ยง A.3
   426  	if opErr.Err.Error() != badCertificate {
   427  		t.Fatalf("expected tls.alert bad_certificate but received: %q", opErr.Err.Error())
   428  	}
   429  
   430  	// PASS: Requests that specify a valid hostname, CA cert, and client
   431  	// certificate succeed.
   432  	tlsConf.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
   433  		c, err := tls.LoadX509KeyPair(foocert, fookey)
   434  		if err != nil {
   435  			return nil, err
   436  		}
   437  		return &c, nil
   438  	}
   439  	transport = &http.Transport{TLSClientConfig: tlsConf}
   440  	client = &http.Client{Transport: transport}
   441  	req, err = http.NewRequest("GET", reqURL, nil)
   442  	if err != nil {
   443  		t.Fatalf("error creating request: %v", err)
   444  	}
   445  	resp, err = client.Do(req)
   446  	if err != nil {
   447  		t.Fatalf("unexpected error: %v", err)
   448  	}
   449  	resp.Body.Close()
   450  	if resp.StatusCode != 200 {
   451  		t.Fatalf("expected 200 status code but got: %d", resp.StatusCode)
   452  	}
   453  }
   454  
   455  // assertIndex tests that X-Nomad-Index is set and non-zero
   456  func assertIndex(t *testing.T, resp *httptest.ResponseRecorder) {
   457  	header := resp.Header().Get("X-Nomad-Index")
   458  	if header == "" || header == "0" {
   459  		t.Fatalf("Bad: %v", header)
   460  	}
   461  }
   462  
   463  // checkIndex is like assertIndex but returns an error
   464  func checkIndex(resp *httptest.ResponseRecorder) error {
   465  	header := resp.Header().Get("X-Nomad-Index")
   466  	if header == "" || header == "0" {
   467  		return fmt.Errorf("Bad: %v", header)
   468  	}
   469  	return nil
   470  }
   471  
   472  // getIndex parses X-Nomad-Index
   473  func getIndex(t *testing.T, resp *httptest.ResponseRecorder) uint64 {
   474  	header := resp.Header().Get("X-Nomad-Index")
   475  	if header == "" {
   476  		t.Fatalf("Bad: %v", header)
   477  	}
   478  	val, err := strconv.Atoi(header)
   479  	if err != nil {
   480  		t.Fatalf("Bad: %v", header)
   481  	}
   482  	return uint64(val)
   483  }
   484  
   485  func httpTest(t testing.TB, cb func(c *Config), f func(srv *TestAgent)) {
   486  	s := makeHTTPServer(t, cb)
   487  	defer s.Shutdown()
   488  	testutil.WaitForLeader(t, s.Agent.RPC)
   489  	f(s)
   490  }
   491  
   492  func encodeReq(obj interface{}) io.ReadCloser {
   493  	buf := bytes.NewBuffer(nil)
   494  	enc := json.NewEncoder(buf)
   495  	enc.Encode(obj)
   496  	return ioutil.NopCloser(buf)
   497  }