diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca5fc4ab4..a4864f15c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,48 @@ BREAKING CHANGES: -* **Raft Protocol Defaults to 3:** The [`-raft-protocol`](https://www.consul.io/docs/agent/options.html#_raft_protocol) default has been changed from 2 to 3, enabling all Autopilot features by default. Version 3 requires Consul running 0.8.0 or newer on all servers in order to work, so if you are upgrading with older servers in a cluster then you will need to set this back to 2 in order to upgrade. See [Raft Protocol Version Compatibility](https://www.consul.io/docs/upgrade-specific.html#raft-protocol-version-compatibility) for more details. Also the format of `peers.json` used for outage recovery is different when running with the lastest Raft protocol. See [Manual Recovery Using peers.json](https://www.consul.io/docs/guides/outage.html#manual-recovery-using-peers-json) for a description of the required format. +* **Raft Protocol Defaults to 3:** The [`-raft-protocol`](https://www.consul.io/docs/agent/options.html#_raft_protocol) default has been changed from 2 to 3, enabling all Autopilot features by default. Version 3 requires Consul running 0.8.0 or newer on all servers in order to work, so if you are upgrading with older servers in a cluster then you will need to set this back to 2 in order to upgrade. See [Raft Protocol Version Compatibility](https://www.consul.io/docs/upgrade-specific.html#raft-protocol-version-compatibility) for more details. Also the format of `peers.json` used for outage recovery is different when running with the lastest Raft protocol. See [Manual Recovery Using peers.json](https://www.consul.io/docs/guides/outage.html#manual-recovery-using-peers-json) for a description of the required format. [GH-3477] +* **HTTP Verb Enforcement:** Many endpoints in the HTTP API that previously took any HTTP verb now check for specific HTTP verbs and enforce them. This may break clients relying on the old behavior. The table below has the endpoints that were updated. [GH-3405] + | Endpoint | Required Verbs | + | -------- | -------------- | + | /v1/acl/info | GET | + | /v1/acl/list | GET | + | /v1/acl/replication | GET | + | /v1/agent/check/deregister | PUT | + | /v1/agent/check/fail | PUT | + | /v1/agent/check/pass | PUT | + | /v1/agent/check/register | PUT | + | /v1/agent/check/warn | PUT | + | /v1/agent/checks | GET | + | /v1/agent/force-leave | PUT | + | /v1/agent/join | PUT | + | /v1/agent/members | GET | + | /v1/agent/metrics | GET | + | /v1/agent/self | GET | + | /v1/agent/service/register | PUT | + | /v1/agent/service/deregister | PUT | + | /v1/agent/services | GET | + | /v1/catalog/datacenters | GET | + | /v1/catalog/deregister | PUT | + | /v1/catalog/node | GET | + | /v1/catalog/nodes | GET | + | /v1/catalog/register | PUT | + | /v1/catalog/service | GET | + | /v1/catalog/services | GET | + | /v1/coordinate/datacenters | GET | + | /v1/coordinate/nodes | GET | + | /v1/health/checks | GET | + | /v1/health/node | GET | + | /v1/health/service | GET | + | /v1/health/state | GET | + | /v1/internal/ui/node | GET | + | /v1/internal/ui/nodes | GET | + | /v1/internal/ui/services | GET | + | /v1/session/info | GET | + | /v1/session/list | GET | + | /v1/session/node | GET | + | /v1/status/leader | GET | + | /v1/status/peers | GET | FEATURES: diff --git a/agent/acl_endpoint.go b/agent/acl_endpoint.go index 10f04a6276..c30f5f55ef 100644 --- a/agent/acl_endpoint.go +++ b/agent/acl_endpoint.go @@ -25,8 +25,7 @@ func ACLDisabled(resp http.ResponseWriter, req *http.Request) (interface{}, erro // a cluster to get the first management token. func (s *HTTPServer) ACLBootstrap(resp http.ResponseWriter, req *http.Request) (interface{}, error) { if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } args := structs.DCSpecificRequest{ @@ -49,10 +48,8 @@ func (s *HTTPServer) ACLBootstrap(resp http.ResponseWriter, req *http.Request) ( } func (s *HTTPServer) ACLDestroy(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - // Mandate a PUT request if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } args := structs.ACLRequest{ @@ -77,18 +74,22 @@ func (s *HTTPServer) ACLDestroy(resp http.ResponseWriter, req *http.Request) (in } func (s *HTTPServer) ACLCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } return s.aclSet(resp, req, false) } func (s *HTTPServer) ACLUpdate(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } return s.aclSet(resp, req, true) } func (s *HTTPServer) aclSet(resp http.ResponseWriter, req *http.Request, update bool) (interface{}, error) { - // Mandate a PUT request if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } args := structs.ACLRequest{ @@ -128,10 +129,8 @@ func (s *HTTPServer) aclSet(resp http.ResponseWriter, req *http.Request, update } func (s *HTTPServer) ACLClone(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - // Mandate a PUT request if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } args := structs.ACLSpecificRequest{ @@ -182,6 +181,10 @@ func (s *HTTPServer) ACLClone(resp http.ResponseWriter, req *http.Request) (inte } func (s *HTTPServer) ACLGet(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + args := structs.ACLSpecificRequest{ Datacenter: s.agent.config.ACLDatacenter, } @@ -212,6 +215,10 @@ func (s *HTTPServer) ACLGet(resp http.ResponseWriter, req *http.Request) (interf } func (s *HTTPServer) ACLList(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + args := structs.DCSpecificRequest{ Datacenter: s.agent.config.ACLDatacenter, } @@ -234,6 +241,10 @@ func (s *HTTPServer) ACLList(resp http.ResponseWriter, req *http.Request) (inter } func (s *HTTPServer) ACLReplicationStatus(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Note that we do not forward to the ACL DC here. This is a query for // any DC that's doing replication. args := structs.DCSpecificRequest{} diff --git a/agent/acl_endpoint_test.go b/agent/acl_endpoint_test.go index 4e5ad63004..9aba5154c3 100644 --- a/agent/acl_endpoint_test.go +++ b/agent/acl_endpoint_test.go @@ -44,7 +44,6 @@ func TestACL_Bootstrap(t *testing.T) { code int token bool }{ - {"bad method", "GET", http.StatusMethodNotAllowed, false}, {"bootstrap", "PUT", http.StatusOK, true}, {"not again", "PUT", http.StatusForbidden, false}, } diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 2d49fa6930..551b9cf071 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -29,6 +29,10 @@ type Self struct { } func (s *HTTPServer) AgentSelf(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + var cs lib.CoordinateSet if !s.agent.config.DisableCoordinates { var err error @@ -58,6 +62,10 @@ func (s *HTTPServer) AgentSelf(resp http.ResponseWriter, req *http.Request) (int } func (s *HTTPServer) AgentMetrics(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Fetch the ACL token, if any, and enforce agent policy. var token string s.parseToken(req, &token) @@ -74,8 +82,7 @@ func (s *HTTPServer) AgentMetrics(resp http.ResponseWriter, req *http.Request) ( func (s *HTTPServer) AgentReload(resp http.ResponseWriter, req *http.Request) (interface{}, error) { if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } // Fetch the ACL token, if any, and enforce agent policy. @@ -107,6 +114,10 @@ func (s *HTTPServer) AgentReload(resp http.ResponseWriter, req *http.Request) (i } func (s *HTTPServer) AgentServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Fetch the ACL token, if any. var token string s.parseToken(req, &token) @@ -127,6 +138,10 @@ func (s *HTTPServer) AgentServices(resp http.ResponseWriter, req *http.Request) } func (s *HTTPServer) AgentChecks(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Fetch the ACL token, if any. var token string s.parseToken(req, &token) @@ -147,6 +162,10 @@ func (s *HTTPServer) AgentChecks(resp http.ResponseWriter, req *http.Request) (i } func (s *HTTPServer) AgentMembers(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Fetch the ACL token, if any. var token string s.parseToken(req, &token) @@ -192,6 +211,10 @@ func (s *HTTPServer) AgentMembers(resp http.ResponseWriter, req *http.Request) ( } func (s *HTTPServer) AgentJoin(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } + // Fetch the ACL token, if any, and enforce agent policy. var token string s.parseToken(req, &token) @@ -221,8 +244,7 @@ func (s *HTTPServer) AgentJoin(resp http.ResponseWriter, req *http.Request) (int func (s *HTTPServer) AgentLeave(resp http.ResponseWriter, req *http.Request) (interface{}, error) { if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } // Fetch the ACL token, if any, and enforce agent policy. @@ -243,6 +265,10 @@ func (s *HTTPServer) AgentLeave(resp http.ResponseWriter, req *http.Request) (in } func (s *HTTPServer) AgentForceLeave(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } + // Fetch the ACL token, if any, and enforce agent policy. var token string s.parseToken(req, &token) @@ -270,6 +296,10 @@ func (s *HTTPServer) syncChanges() { const invalidCheckMessage = "Must provide TTL or Script/DockerContainerID/HTTP/TCP and Interval" func (s *HTTPServer) AgentRegisterCheck(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } + var args structs.CheckDefinition // Fixup the type decode of TTL or Interval. decodeCB := func(raw interface{}) error { @@ -321,6 +351,10 @@ func (s *HTTPServer) AgentRegisterCheck(resp http.ResponseWriter, req *http.Requ } func (s *HTTPServer) AgentDeregisterCheck(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } + checkID := types.CheckID(strings.TrimPrefix(req.URL.Path, "/v1/agent/check/deregister/")) // Get the provided token, if any, and vet against any ACL policies. @@ -338,6 +372,10 @@ func (s *HTTPServer) AgentDeregisterCheck(resp http.ResponseWriter, req *http.Re } func (s *HTTPServer) AgentCheckPass(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } + checkID := types.CheckID(strings.TrimPrefix(req.URL.Path, "/v1/agent/check/pass/")) note := req.URL.Query().Get("note") @@ -356,6 +394,10 @@ func (s *HTTPServer) AgentCheckPass(resp http.ResponseWriter, req *http.Request) } func (s *HTTPServer) AgentCheckWarn(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } + checkID := types.CheckID(strings.TrimPrefix(req.URL.Path, "/v1/agent/check/warn/")) note := req.URL.Query().Get("note") @@ -374,6 +416,10 @@ func (s *HTTPServer) AgentCheckWarn(resp http.ResponseWriter, req *http.Request) } func (s *HTTPServer) AgentCheckFail(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } + checkID := types.CheckID(strings.TrimPrefix(req.URL.Path, "/v1/agent/check/fail/")) note := req.URL.Query().Get("note") @@ -408,8 +454,7 @@ type checkUpdate struct { // APIs. func (s *HTTPServer) AgentCheckUpdate(resp http.ResponseWriter, req *http.Request) (interface{}, error) { if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } var update checkUpdate @@ -452,6 +497,10 @@ func (s *HTTPServer) AgentCheckUpdate(resp http.ResponseWriter, req *http.Reques } func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } + var args structs.ServiceDefinition // Fixup the type decode of TTL or Interval if a check if provided. decodeCB := func(raw interface{}) error { @@ -535,6 +584,10 @@ func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Re } func (s *HTTPServer) AgentDeregisterService(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } + serviceID := strings.TrimPrefix(req.URL.Path, "/v1/agent/service/deregister/") // Get the provided token, if any, and vet against any ACL policies. @@ -552,10 +605,8 @@ func (s *HTTPServer) AgentDeregisterService(resp http.ResponseWriter, req *http. } func (s *HTTPServer) AgentServiceMaintenance(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - // Only PUT supported if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } // Ensure we have a service ID @@ -608,10 +659,8 @@ func (s *HTTPServer) AgentServiceMaintenance(resp http.ResponseWriter, req *http } func (s *HTTPServer) AgentNodeMaintenance(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - // Only PUT supported if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } // Ensure we have some action @@ -651,10 +700,8 @@ func (s *HTTPServer) AgentNodeMaintenance(resp http.ResponseWriter, req *http.Re } func (s *HTTPServer) AgentMonitor(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - // Only GET supported. if req.Method != "GET" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} } // Fetch the ACL token, if any, and enforce agent policy. @@ -741,8 +788,7 @@ func (h *httpLogHandler) HandleLog(log string) { func (s *HTTPServer) AgentToken(resp http.ResponseWriter, req *http.Request) (interface{}, error) { if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } // Fetch the ACL token, if any, and enforce agent policy. diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index 6504dcd10a..bbc0f3b93f 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -421,7 +421,7 @@ func TestAgent_Join(t *testing.T) { defer a2.Shutdown() addr := fmt.Sprintf("127.0.0.1:%d", a2.Config.SerfPortLAN) - req, _ := http.NewRequest("GET", fmt.Sprintf("/v1/agent/join/%s", addr), nil) + req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/agent/join/%s", addr), nil) obj, err := a1.srv.AgentJoin(nil, req) if err != nil { t.Fatalf("Err: %v", err) @@ -449,7 +449,7 @@ func TestAgent_Join_WAN(t *testing.T) { defer a2.Shutdown() addr := fmt.Sprintf("127.0.0.1:%d", a2.Config.SerfPortWAN) - req, _ := http.NewRequest("GET", fmt.Sprintf("/v1/agent/join/%s?wan=true", addr), nil) + req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/agent/join/%s?wan=true", addr), nil) obj, err := a1.srv.AgentJoin(nil, req) if err != nil { t.Fatalf("Err: %v", err) @@ -479,14 +479,14 @@ func TestAgent_Join_ACLDeny(t *testing.T) { addr := fmt.Sprintf("127.0.0.1:%d", a2.Config.SerfPortLAN) t.Run("no token", func(t *testing.T) { - req, _ := http.NewRequest("GET", fmt.Sprintf("/v1/agent/join/%s", addr), nil) + req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/agent/join/%s", addr), nil) if _, err := a1.srv.AgentJoin(nil, req); !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } }) t.Run("agent master token", func(t *testing.T) { - req, _ := http.NewRequest("GET", fmt.Sprintf("/v1/agent/join/%s?token=towel", addr), nil) + req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/agent/join/%s?token=towel", addr), nil) _, err := a1.srv.AgentJoin(nil, req) if err != nil { t.Fatalf("err: %v", err) @@ -495,7 +495,7 @@ func TestAgent_Join_ACLDeny(t *testing.T) { t.Run("read-only token", func(t *testing.T) { ro := makeReadOnlyAgentACL(t, a1.srv) - req, _ := http.NewRequest("GET", fmt.Sprintf("/v1/agent/join/%s?token=%s", addr, ro), nil) + req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/agent/join/%s?token=%s", addr, ro), nil) if _, err := a1.srv.AgentJoin(nil, req); !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } @@ -616,7 +616,7 @@ func TestAgent_ForceLeave(t *testing.T) { a2.Shutdown() // Force leave now - req, _ := http.NewRequest("GET", fmt.Sprintf("/v1/agent/force-leave/%s", a2.Config.NodeName), nil) + req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/agent/force-leave/%s", a2.Config.NodeName), nil) obj, err := a1.srv.AgentForceLeave(nil, req) if err != nil { t.Fatalf("Err: %v", err) @@ -639,14 +639,14 @@ func TestAgent_ForceLeave_ACLDeny(t *testing.T) { defer a.Shutdown() t.Run("no token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/force-leave/nope", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/force-leave/nope", nil) if _, err := a.srv.AgentForceLeave(nil, req); !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } }) t.Run("agent master token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/force-leave/nope?token=towel", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/force-leave/nope?token=towel", nil) if _, err := a.srv.AgentForceLeave(nil, req); err != nil { t.Fatalf("err: %v", err) } @@ -654,7 +654,7 @@ func TestAgent_ForceLeave_ACLDeny(t *testing.T) { t.Run("read-only token", func(t *testing.T) { ro := makeReadOnlyAgentACL(t, a.srv) - req, _ := http.NewRequest("GET", fmt.Sprintf("/v1/agent/force-leave/nope?token=%s", ro), nil) + req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/agent/force-leave/nope?token=%s", ro), nil) if _, err := a.srv.AgentForceLeave(nil, req); !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } @@ -671,7 +671,7 @@ func TestAgent_RegisterCheck(t *testing.T) { Name: "test", TTL: 15 * time.Second, } - req, _ := http.NewRequest("GET", "/v1/agent/check/register?token=abc123", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/agent/check/register?token=abc123", jsonReader(args)) obj, err := a.srv.AgentRegisterCheck(nil, req) if err != nil { t.Fatalf("err: %v", err) @@ -713,7 +713,7 @@ func TestAgent_RegisterCheck_Passing(t *testing.T) { TTL: 15 * time.Second, Status: api.HealthPassing, } - req, _ := http.NewRequest("GET", "/v1/agent/check/register", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/agent/check/register", jsonReader(args)) obj, err := a.srv.AgentRegisterCheck(nil, req) if err != nil { t.Fatalf("err: %v", err) @@ -749,7 +749,7 @@ func TestAgent_RegisterCheck_BadStatus(t *testing.T) { TTL: 15 * time.Second, Status: "fluffy", } - req, _ := http.NewRequest("GET", "/v1/agent/check/register", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/agent/check/register", jsonReader(args)) resp := httptest.NewRecorder() if _, err := a.srv.AgentRegisterCheck(resp, req); err != nil { t.Fatalf("err: %v", err) @@ -770,14 +770,14 @@ func TestAgent_RegisterCheck_ACLDeny(t *testing.T) { } t.Run("no token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/check/register", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/agent/check/register", jsonReader(args)) if _, err := a.srv.AgentRegisterCheck(nil, req); !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } }) t.Run("root token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/check/register?token=root", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/agent/check/register?token=root", jsonReader(args)) if _, err := a.srv.AgentRegisterCheck(nil, req); err != nil { t.Fatalf("err: %v", err) } @@ -795,7 +795,7 @@ func TestAgent_DeregisterCheck(t *testing.T) { } // Register node - req, _ := http.NewRequest("GET", "/v1/agent/check/deregister/test", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/check/deregister/test", nil) obj, err := a.srv.AgentDeregisterCheck(nil, req) if err != nil { t.Fatalf("err: %v", err) @@ -821,14 +821,14 @@ func TestAgent_DeregisterCheckACLDeny(t *testing.T) { } t.Run("no token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/check/deregister/test", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/check/deregister/test", nil) if _, err := a.srv.AgentDeregisterCheck(nil, req); !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } }) t.Run("root token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/check/deregister/test?token=root", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/check/deregister/test?token=root", nil) if _, err := a.srv.AgentDeregisterCheck(nil, req); err != nil { t.Fatalf("err: %v", err) } @@ -846,7 +846,7 @@ func TestAgent_PassCheck(t *testing.T) { t.Fatalf("err: %v", err) } - req, _ := http.NewRequest("GET", "/v1/agent/check/pass/test", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/check/pass/test", nil) obj, err := a.srv.AgentCheckPass(nil, req) if err != nil { t.Fatalf("err: %v", err) @@ -874,14 +874,14 @@ func TestAgent_PassCheck_ACLDeny(t *testing.T) { } t.Run("no token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/check/pass/test", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/check/pass/test", nil) if _, err := a.srv.AgentCheckPass(nil, req); !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } }) t.Run("root token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/check/pass/test?token=root", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/check/pass/test?token=root", nil) if _, err := a.srv.AgentCheckPass(nil, req); err != nil { t.Fatalf("err: %v", err) } @@ -899,7 +899,7 @@ func TestAgent_WarnCheck(t *testing.T) { t.Fatalf("err: %v", err) } - req, _ := http.NewRequest("GET", "/v1/agent/check/warn/test", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/check/warn/test", nil) obj, err := a.srv.AgentCheckWarn(nil, req) if err != nil { t.Fatalf("err: %v", err) @@ -927,14 +927,14 @@ func TestAgent_WarnCheck_ACLDeny(t *testing.T) { } t.Run("no token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/check/warn/test", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/check/warn/test", nil) if _, err := a.srv.AgentCheckWarn(nil, req); !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } }) t.Run("root token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/check/warn/test?token=root", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/check/warn/test?token=root", nil) if _, err := a.srv.AgentCheckWarn(nil, req); err != nil { t.Fatalf("err: %v", err) } @@ -952,7 +952,7 @@ func TestAgent_FailCheck(t *testing.T) { t.Fatalf("err: %v", err) } - req, _ := http.NewRequest("GET", "/v1/agent/check/fail/test", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/check/fail/test", nil) obj, err := a.srv.AgentCheckFail(nil, req) if err != nil { t.Fatalf("err: %v", err) @@ -980,14 +980,14 @@ func TestAgent_FailCheck_ACLDeny(t *testing.T) { } t.Run("no token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/check/fail/test", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/check/fail/test", nil) if _, err := a.srv.AgentCheckFail(nil, req); !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } }) t.Run("root token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/check/fail/test?token=root", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/check/fail/test?token=root", nil) if _, err := a.srv.AgentCheckFail(nil, req); err != nil { t.Fatalf("err: %v", err) } @@ -1075,22 +1075,6 @@ func TestAgent_UpdateCheck(t *testing.T) { t.Fatalf("expected 400, got %d", resp.Code) } }) - - t.Run("bogus verb", func(t *testing.T) { - args := checkUpdate{Status: api.HealthPassing} - req, _ := http.NewRequest("POST", "/v1/agent/check/update/test", jsonReader(args)) - resp := httptest.NewRecorder() - obj, err := a.srv.AgentCheckUpdate(resp, req) - if err != nil { - t.Fatalf("err: %v", err) - } - if obj != nil { - t.Fatalf("bad: %v", obj) - } - if resp.Code != 405 { - t.Fatalf("expected 405, got %d", resp.Code) - } - }) } func TestAgent_UpdateCheck_ACLDeny(t *testing.T) { @@ -1142,7 +1126,7 @@ func TestAgent_RegisterService(t *testing.T) { }, }, } - req, _ := http.NewRequest("GET", "/v1/agent/service/register?token=abc123", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/agent/service/register?token=abc123", jsonReader(args)) obj, err := a.srv.AgentRegisterService(nil, req) if err != nil { @@ -1196,14 +1180,14 @@ func TestAgent_RegisterService_ACLDeny(t *testing.T) { } t.Run("no token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/service/register", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/agent/service/register", jsonReader(args)) if _, err := a.srv.AgentRegisterService(nil, req); !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } }) t.Run("root token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/service/register?token=root", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/agent/service/register?token=root", jsonReader(args)) if _, err := a.srv.AgentRegisterService(nil, req); err != nil { t.Fatalf("err: %v", err) } @@ -1222,7 +1206,7 @@ func TestAgent_RegisterService_InvalidAddress(t *testing.T) { Address: addr, Port: 8000, } - req, _ := http.NewRequest("GET", "/v1/agent/service/register?token=abc123", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/agent/service/register?token=abc123", jsonReader(args)) resp := httptest.NewRecorder() _, err := a.srv.AgentRegisterService(resp, req) if err != nil { @@ -1251,7 +1235,7 @@ func TestAgent_DeregisterService(t *testing.T) { t.Fatalf("err: %v", err) } - req, _ := http.NewRequest("GET", "/v1/agent/service/deregister/test", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/service/deregister/test", nil) obj, err := a.srv.AgentDeregisterService(nil, req) if err != nil { t.Fatalf("err: %v", err) @@ -1284,14 +1268,14 @@ func TestAgent_DeregisterService_ACLDeny(t *testing.T) { } t.Run("no token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/service/deregister/test", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/service/deregister/test", nil) if _, err := a.srv.AgentDeregisterService(nil, req); !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } }) t.Run("root token", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/service/deregister/test?token=root", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/service/deregister/test?token=root", nil) if _, err := a.srv.AgentDeregisterService(nil, req); err != nil { t.Fatalf("err: %v", err) } @@ -1303,17 +1287,6 @@ func TestAgent_ServiceMaintenance_BadRequest(t *testing.T) { a := NewTestAgent(t.Name(), "") defer a.Shutdown() - t.Run("not PUT", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/v1/agent/service/maintenance/test?enable=true", nil) - resp := httptest.NewRecorder() - if _, err := a.srv.AgentServiceMaintenance(resp, req); err != nil { - t.Fatalf("err: %s", err) - } - if resp.Code != 405 { - t.Fatalf("expected 405, got %d", resp.Code) - } - }) - t.Run("not enabled", func(t *testing.T) { req, _ := http.NewRequest("PUT", "/v1/agent/service/maintenance/test", nil) resp := httptest.NewRecorder() @@ -1460,19 +1433,9 @@ func TestAgent_NodeMaintenance_BadRequest(t *testing.T) { a := NewTestAgent(t.Name(), "") defer a.Shutdown() - // Fails on non-PUT - req, _ := http.NewRequest("GET", "/v1/agent/self/maintenance?enable=true", nil) - resp := httptest.NewRecorder() - if _, err := a.srv.AgentNodeMaintenance(resp, req); err != nil { - t.Fatalf("err: %s", err) - } - if resp.Code != 405 { - t.Fatalf("expected 405, got %d", resp.Code) - } - // Fails when no enable flag provided - req, _ = http.NewRequest("PUT", "/v1/agent/self/maintenance", nil) - resp = httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/v1/agent/self/maintenance", nil) + resp := httptest.NewRecorder() if _, err := a.srv.AgentNodeMaintenance(resp, req); err != nil { t.Fatalf("err: %s", err) } @@ -1571,7 +1534,7 @@ func TestAgent_RegisterCheck_Service(t *testing.T) { } // First register the service - req, _ := http.NewRequest("GET", "/v1/agent/service/register", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/agent/service/register", jsonReader(args)) if _, err := a.srv.AgentRegisterService(nil, req); err != nil { t.Fatalf("err: %v", err) } @@ -1582,7 +1545,7 @@ func TestAgent_RegisterCheck_Service(t *testing.T) { ServiceID: "memcache", TTL: 15 * time.Second, } - req, _ = http.NewRequest("GET", "/v1/agent/check/register", jsonReader(checkArgs)) + req, _ = http.NewRequest("PUT", "/v1/agent/check/register", jsonReader(checkArgs)) if _, err := a.srv.AgentRegisterCheck(nil, req); err != nil { t.Fatalf("err: %v", err) } @@ -1721,12 +1684,6 @@ func TestAgent_Token(t *testing.T) { code int got, want tokens }{ - { - name: "bad method", - method: "GET", - url: "acl_token", - code: http.StatusMethodNotAllowed, - }, { name: "bad token name", method: "PUT", diff --git a/agent/catalog_endpoint.go b/agent/catalog_endpoint.go index bcad6bfa5e..175b0d4a26 100644 --- a/agent/catalog_endpoint.go +++ b/agent/catalog_endpoint.go @@ -9,6 +9,10 @@ import ( ) func (s *HTTPServer) CatalogRegister(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } + var args structs.RegisterRequest if err := decodeBody(req, &args, nil); err != nil { resp.WriteHeader(http.StatusBadRequest) @@ -31,6 +35,10 @@ func (s *HTTPServer) CatalogRegister(resp http.ResponseWriter, req *http.Request } func (s *HTTPServer) CatalogDeregister(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} + } + var args structs.DeregisterRequest if err := decodeBody(req, &args, nil); err != nil { resp.WriteHeader(http.StatusBadRequest) @@ -53,6 +61,10 @@ func (s *HTTPServer) CatalogDeregister(resp http.ResponseWriter, req *http.Reque } func (s *HTTPServer) CatalogDatacenters(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + var out []string if err := s.agent.RPC("Catalog.ListDatacenters", struct{}{}, &out); err != nil { return nil, err @@ -61,6 +73,10 @@ func (s *HTTPServer) CatalogDatacenters(resp http.ResponseWriter, req *http.Requ } func (s *HTTPServer) CatalogNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Setup the request args := structs.DCSpecificRequest{} s.parseSource(req, &args.Source) @@ -84,6 +100,10 @@ func (s *HTTPServer) CatalogNodes(resp http.ResponseWriter, req *http.Request) ( } func (s *HTTPServer) CatalogServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Set default DC args := structs.DCSpecificRequest{} args.NodeMetaFilters = s.parseMetaFilter(req) @@ -105,6 +125,10 @@ func (s *HTTPServer) CatalogServices(resp http.ResponseWriter, req *http.Request } func (s *HTTPServer) CatalogServiceNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Set default DC args := structs.ServiceSpecificRequest{} s.parseSource(req, &args.Source) @@ -149,6 +173,10 @@ func (s *HTTPServer) CatalogServiceNodes(resp http.ResponseWriter, req *http.Req } func (s *HTTPServer) CatalogNodeServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Set default Datacenter args := structs.NodeSpecificRequest{} if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { diff --git a/agent/catalog_endpoint_test.go b/agent/catalog_endpoint_test.go index 3ac20afa03..49fd01d7a1 100644 --- a/agent/catalog_endpoint_test.go +++ b/agent/catalog_endpoint_test.go @@ -22,7 +22,7 @@ func TestCatalogRegister(t *testing.T) { Node: "foo", Address: "127.0.0.1", } - req, _ := http.NewRequest("GET", "/v1/catalog/register", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/catalog/register", jsonReader(args)) obj, err := a.srv.CatalogRegister(nil, req) if err != nil { t.Fatalf("err: %v", err) @@ -67,7 +67,7 @@ func TestCatalogRegister_Service_InvalidAddress(t *testing.T) { Port: 8080, }, } - req, _ := http.NewRequest("GET", "/v1/catalog/register", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/catalog/register", jsonReader(args)) _, err := a.srv.CatalogRegister(nil, req) if err == nil || err.Error() != "Invalid service address" { t.Fatalf("err: %v", err) @@ -83,7 +83,7 @@ func TestCatalogDeregister(t *testing.T) { // Register node args := &structs.DeregisterRequest{Node: "foo"} - req, _ := http.NewRequest("GET", "/v1/catalog/deregister", jsonReader(args)) + req, _ := http.NewRequest("PUT", "/v1/catalog/deregister", jsonReader(args)) obj, err := a.srv.CatalogDeregister(nil, req) if err != nil { t.Fatalf("err: %v", err) @@ -101,7 +101,8 @@ func TestCatalogDatacenters(t *testing.T) { defer a.Shutdown() retry.Run(t, func(r *retry.R) { - obj, err := a.srv.CatalogDatacenters(nil, nil) + req, _ := http.NewRequest("GET", "/v1/catalog/datacenters", nil) + obj, err := a.srv.CatalogDatacenters(nil, req) if err != nil { r.Fatal(err) } diff --git a/agent/coordinate_endpoint.go b/agent/coordinate_endpoint.go index e602dacbed..e9ac566d95 100644 --- a/agent/coordinate_endpoint.go +++ b/agent/coordinate_endpoint.go @@ -40,6 +40,10 @@ func (s *sorter) Less(i, j int) bool { // CoordinateDatacenters returns the WAN nodes in each datacenter, along with // raw network coordinates. func (s *HTTPServer) CoordinateDatacenters(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + var out []structs.DatacenterMap if err := s.agent.RPC("Coordinate.ListDatacenters", struct{}{}, &out); err != nil { for i := range out { @@ -65,6 +69,10 @@ func (s *HTTPServer) CoordinateDatacenters(resp http.ResponseWriter, req *http.R // CoordinateNodes returns the LAN nodes in the given datacenter, along with // raw network coordinates. func (s *HTTPServer) CoordinateNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + args := structs.DCSpecificRequest{} if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { return nil, nil diff --git a/agent/event_endpoint.go b/agent/event_endpoint.go index db8c46d4df..132106cc17 100644 --- a/agent/event_endpoint.go +++ b/agent/event_endpoint.go @@ -20,10 +20,8 @@ const ( // EventFire is used to fire a new event func (s *HTTPServer) EventFire(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - // Mandate a PUT request if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } // Get the datacenter @@ -79,6 +77,10 @@ func (s *HTTPServer) EventFire(resp http.ResponseWriter, req *http.Request) (int // EventList is used to retrieve the recent list of events func (s *HTTPServer) EventList(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Parse the query options, since we simulate a blocking query var b structs.QueryOptions if parseWait(resp, req, &b) { diff --git a/agent/health_endpoint.go b/agent/health_endpoint.go index 33a55bb498..59ea6c02fd 100644 --- a/agent/health_endpoint.go +++ b/agent/health_endpoint.go @@ -11,6 +11,10 @@ import ( ) func (s *HTTPServer) HealthChecksInState(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Set default DC args := structs.ChecksInStateRequest{} s.parseSource(req, &args.Source) @@ -47,6 +51,10 @@ func (s *HTTPServer) HealthChecksInState(resp http.ResponseWriter, req *http.Req } func (s *HTTPServer) HealthNodeChecks(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Set default DC args := structs.NodeSpecificRequest{} if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { @@ -81,6 +89,10 @@ func (s *HTTPServer) HealthNodeChecks(resp http.ResponseWriter, req *http.Reques } func (s *HTTPServer) HealthServiceChecks(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Set default DC args := structs.ServiceSpecificRequest{} s.parseSource(req, &args.Source) @@ -117,6 +129,10 @@ func (s *HTTPServer) HealthServiceChecks(resp http.ResponseWriter, req *http.Req } func (s *HTTPServer) HealthServiceNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Set default DC args := structs.ServiceSpecificRequest{} s.parseSource(req, &args.Source) diff --git a/agent/http.go b/agent/http.go index 229b81ee9b..e54c8d202e 100644 --- a/agent/http.go +++ b/agent/http.go @@ -18,6 +18,16 @@ import ( "github.com/mitchellh/mapstructure" ) +// MethodNotAllowedError should be returned by a handler when the HTTP method is not allowed. +type MethodNotAllowedError struct { + Method string + Allow []string +} + +func (e MethodNotAllowedError) Error() string { + return fmt.Sprintf("method %s not allowed", e.Method) +} + // HTTPServer provides an HTTP api for an agent. type HTTPServer struct { *http.Server @@ -234,6 +244,11 @@ func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Reque return } + isMethodNotAllowed := func(err error) bool { + _, ok := err.(MethodNotAllowedError) + return ok + } + handleErr := func(err error) { s.agent.logger.Printf("[ERR] http: Request %s %v, error: %v from=%s", req.Method, logURL, err, req.RemoteAddr) switch { @@ -242,6 +257,13 @@ func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Reque fmt.Fprint(resp, err.Error()) case structs.IsErrRPCRateExceeded(err): resp.WriteHeader(http.StatusTooManyRequests) + case isMethodNotAllowed(err): + // RFC2616 states that for 405 Method Not Allowed the response + // MUST include an Allow header containing the list of valid + // methods for the requested resource. + // https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + resp.Header()["Allow"] = err.(MethodNotAllowedError).Allow + resp.WriteHeader(http.StatusMethodNotAllowed) // 405 fmt.Fprint(resp, err.Error()) default: resp.WriteHeader(http.StatusInternalServerError) diff --git a/agent/http_test.go b/agent/http_test.go index d607d46258..3e5da26d33 100644 --- a/agent/http_test.go +++ b/agent/http_test.go @@ -19,6 +19,7 @@ import ( "time" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/logger" "github.com/hashicorp/consul/testutil" "github.com/hashicorp/go-cleanhttp" ) @@ -307,6 +308,114 @@ func TestHTTPAPIResponseHeaders(t *testing.T) { } } +func TestHTTPAPI_MethodNotAllowed(t *testing.T) { + tests := []struct { + methods, uri string + }{ + {"PUT", "/v1/acl/bootstrap"}, + {"PUT", "/v1/acl/create"}, + {"PUT", "/v1/acl/update"}, + {"PUT", "/v1/acl/destroy/"}, + {"GET", "/v1/acl/info/"}, + {"PUT", "/v1/acl/clone/"}, + {"GET", "/v1/acl/list"}, + {"GET", "/v1/acl/replication"}, + {"PUT", "/v1/agent/token/"}, + {"GET", "/v1/agent/self"}, + {"GET", "/v1/agent/members"}, + {"PUT", "/v1/agent/check/deregister/"}, + {"PUT", "/v1/agent/check/fail/"}, + {"PUT", "/v1/agent/check/pass/"}, + {"PUT", "/v1/agent/check/register"}, + {"PUT", "/v1/agent/check/update/"}, + {"PUT", "/v1/agent/check/warn/"}, + {"GET", "/v1/agent/checks"}, + {"PUT", "/v1/agent/force-leave/"}, + {"PUT", "/v1/agent/join/"}, + {"PUT", "/v1/agent/leave"}, + {"PUT", "/v1/agent/maintenance"}, + {"GET", "/v1/agent/metrics"}, + // {"GET", "/v1/agent/monitor"}, // requires LogWriter. Hangs if LogWriter is provided + {"PUT", "/v1/agent/reload"}, + {"PUT", "/v1/agent/service/deregister/"}, + {"PUT", "/v1/agent/service/maintenance/"}, + {"PUT", "/v1/agent/service/register"}, + {"GET", "/v1/agent/services"}, + {"GET", "/v1/catalog/datacenters"}, + {"PUT", "/v1/catalog/deregister"}, + {"GET", "/v1/catalog/node/"}, + {"GET", "/v1/catalog/nodes"}, + {"PUT", "/v1/catalog/register"}, + {"GET", "/v1/catalog/service/"}, + {"GET", "/v1/catalog/services"}, + {"GET", "/v1/coordinate/datacenters"}, + {"GET", "/v1/coordinate/nodes"}, + {"PUT", "/v1/event/fire/"}, + {"GET", "/v1/event/list"}, + {"GET", "/v1/health/checks/"}, + {"GET", "/v1/health/node/"}, + {"GET", "/v1/health/service/"}, + {"GET", "/v1/health/state/"}, + {"GET", "/v1/internal/ui/node/"}, + {"GET", "/v1/internal/ui/nodes"}, + {"GET", "/v1/internal/ui/services"}, + {"GET PUT DELETE", "/v1/kv/"}, + {"GET PUT", "/v1/operator/autopilot/configuration"}, + {"GET", "/v1/operator/autopilot/health"}, + {"GET POST PUT DELETE", "/v1/operator/keyring"}, + {"GET", "/v1/operator/raft/configuration"}, + {"DELETE", "/v1/operator/raft/peer"}, + {"GET POST", "/v1/query"}, + {"GET PUT DELETE", "/v1/query/"}, + {"GET", "/v1/query/xxx/execute"}, + {"GET", "/v1/query/xxx/explain"}, + {"PUT", "/v1/session/create"}, + {"PUT", "/v1/session/destroy/"}, + {"GET", "/v1/session/info/"}, + {"GET", "/v1/session/list"}, + {"GET", "/v1/session/node/"}, + {"PUT", "/v1/session/renew/"}, + {"GET PUT", "/v1/snapshot"}, + {"GET", "/v1/status/leader"}, + // {"GET", "/v1/status/peers"},// hangs + {"PUT", "/v1/txn"}, + + // enterprise only + // {"GET POST", "/v1/operator/area"}, + // {"GET PUT DELETE", "/v1/operator/area/"}, + // {"GET", "/v1/operator/area/xxx/members"}, + } + + a := NewTestAgent(t.Name(), `acl_datacenter = "dc1"`) + a.Agent.LogWriter = logger.NewLogWriter(512) + defer a.Shutdown() + + all := []string{"GET", "PUT", "POST", "DELETE", "HEAD"} + client := http.Client{} + + for _, tt := range tests { + for _, m := range all { + + t.Run(m+" "+tt.uri, func(t *testing.T) { + uri := fmt.Sprintf("http://%s%s", a.HTTPAddr(), tt.uri) + req, _ := http.NewRequest(m, uri, nil) + resp, err := client.Do(req) + if err != nil { + t.Fatal("client.Do failed: ", err) + } + + allowed := strings.Contains(tt.methods, m) + if allowed && resp.StatusCode == http.StatusMethodNotAllowed { + t.Fatalf("method allowed: got status code %d want any other code", resp.StatusCode) + } + if !allowed && resp.StatusCode != http.StatusMethodNotAllowed { + t.Fatalf("method not allowed: got status code %d want %d", resp.StatusCode, http.StatusMethodNotAllowed) + } + }) + } + } +} + func TestContentTypeIsJSON(t *testing.T) { t.Parallel() a := NewTestAgent(t.Name(), "") diff --git a/agent/operator_endpoint.go b/agent/operator_endpoint.go index a1381004b8..5a3c0d38a9 100644 --- a/agent/operator_endpoint.go +++ b/agent/operator_endpoint.go @@ -17,8 +17,7 @@ import ( // This supports the stale query mode in case the cluster doesn't have a leader. func (s *HTTPServer) OperatorRaftConfiguration(resp http.ResponseWriter, req *http.Request) (interface{}, error) { if req.Method != "GET" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} } var args structs.DCSpecificRequest @@ -38,8 +37,7 @@ func (s *HTTPServer) OperatorRaftConfiguration(resp http.ResponseWriter, req *ht // removing peers by address. func (s *HTTPServer) OperatorRaftPeer(resp http.ResponseWriter, req *http.Request) (interface{}, error) { if req.Method != "DELETE" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"DELETE"}} } var args structs.RaftRemovePeerRequest @@ -125,8 +123,7 @@ func (s *HTTPServer) OperatorKeyringEndpoint(resp http.ResponseWriter, req *http case "DELETE": return s.KeyringRemove(resp, req, &args) default: - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"GET", "POST", "PUT", "DELETE"}} } } @@ -264,8 +261,7 @@ func (s *HTTPServer) OperatorAutopilotConfiguration(resp http.ResponseWriter, re return reply, nil default: - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT"}} } } @@ -295,8 +291,7 @@ func FixupConfigDurations(raw interface{}) error { // OperatorServerHealth is used to get the health of the servers in the local DC func (s *HTTPServer) OperatorServerHealth(resp http.ResponseWriter, req *http.Request) (interface{}, error) { if req.Method != "GET" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} } var args structs.DCSpecificRequest diff --git a/agent/prepared_query_endpoint.go b/agent/prepared_query_endpoint.go index 1f12729f98..1d8ee7ba27 100644 --- a/agent/prepared_query_endpoint.go +++ b/agent/prepared_query_endpoint.go @@ -10,11 +10,6 @@ import ( "github.com/hashicorp/consul/agent/structs" ) -const ( - preparedQueryExecuteSuffix = "/execute" - preparedQueryExplainSuffix = "/explain" -) - // preparedQueryCreateResponse is used to wrap the query ID. type preparedQueryCreateResponse struct { ID string @@ -71,8 +66,7 @@ func (s *HTTPServer) PreparedQueryGeneral(resp http.ResponseWriter, req *http.Re return s.preparedQueryList(resp, req) default: - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"GET", "POST"}} } } @@ -204,6 +198,10 @@ func (s *HTTPServer) preparedQueryUpdate(id string, resp http.ResponseWriter, re } } + if args.Query == nil { + args.Query = &structs.PreparedQuery{} + } + // Take the ID from the URL, not the embedded one. args.Query.ID = id @@ -235,35 +233,37 @@ func (s *HTTPServer) preparedQueryDelete(id string, resp http.ResponseWriter, re // PreparedQuerySpecific handles all the prepared query requests specific to a // particular query. func (s *HTTPServer) PreparedQuerySpecific(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - id := strings.TrimPrefix(req.URL.Path, "/v1/query/") + path := req.URL.Path + id := strings.TrimPrefix(path, "/v1/query/") - execute, explain := false, false - if strings.HasSuffix(id, preparedQueryExecuteSuffix) { - execute = true - id = strings.TrimSuffix(id, preparedQueryExecuteSuffix) - } else if strings.HasSuffix(id, preparedQueryExplainSuffix) { - explain = true - id = strings.TrimSuffix(id, preparedQueryExplainSuffix) - } - - switch req.Method { - case "GET": - if execute { - return s.preparedQueryExecute(id, resp, req) - } else if explain { - return s.preparedQueryExplain(id, resp, req) - } else { - return s.preparedQueryGet(id, resp, req) + switch { + case strings.HasSuffix(path, "/execute"): + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} } + id = strings.TrimSuffix(id, "/execute") + return s.preparedQueryExecute(id, resp, req) - case "PUT": - return s.preparedQueryUpdate(id, resp, req) - - case "DELETE": - return s.preparedQueryDelete(id, resp, req) + case strings.HasSuffix(path, "/explain"): + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + id = strings.TrimSuffix(id, "/explain") + return s.preparedQueryExplain(id, resp, req) default: - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + switch req.Method { + case "GET": + return s.preparedQueryGet(id, resp, req) + + case "PUT": + return s.preparedQueryUpdate(id, resp, req) + + case "DELETE": + return s.preparedQueryDelete(id, resp, req) + + default: + return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}} + } } } diff --git a/agent/prepared_query_endpoint_test.go b/agent/prepared_query_endpoint_test.go index 5443195b10..58e27df2a5 100644 --- a/agent/prepared_query_endpoint_test.go +++ b/agent/prepared_query_endpoint_test.go @@ -737,39 +737,6 @@ func TestPreparedQuery_Delete(t *testing.T) { } } -func TestPreparedQuery_BadMethods(t *testing.T) { - t.Parallel() - t.Run("", func(t *testing.T) { - a := NewTestAgent(t.Name(), "") - defer a.Shutdown() - - body := bytes.NewBuffer(nil) - req, _ := http.NewRequest("DELETE", "/v1/query", body) - resp := httptest.NewRecorder() - if _, err := a.srv.PreparedQueryGeneral(resp, req); err != nil { - t.Fatalf("err: %v", err) - } - if resp.Code != 405 { - t.Fatalf("bad code: %d", resp.Code) - } - }) - - t.Run("", func(t *testing.T) { - a := NewTestAgent(t.Name(), "") - defer a.Shutdown() - - body := bytes.NewBuffer(nil) - req, _ := http.NewRequest("POST", "/v1/query/my-id", body) - resp := httptest.NewRecorder() - if _, err := a.srv.PreparedQuerySpecific(resp, req); err != nil { - t.Fatalf("err: %v", err) - } - if resp.Code != 405 { - t.Fatalf("bad code: %d", resp.Code) - } - }) -} - func TestPreparedQuery_parseLimit(t *testing.T) { t.Parallel() body := bytes.NewBuffer(nil) diff --git a/agent/session_endpoint.go b/agent/session_endpoint.go index cbb3b51a91..56a90a5904 100644 --- a/agent/session_endpoint.go +++ b/agent/session_endpoint.go @@ -27,10 +27,8 @@ type sessionCreateResponse struct { // SessionCreate is used to create a new session func (s *HTTPServer) SessionCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - // Mandate a PUT request if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } // Default the session to our node + serf check + release session invalidate behavior @@ -107,10 +105,8 @@ func FixupLockDelay(raw interface{}) error { // SessionDestroy is used to destroy an existing session func (s *HTTPServer) SessionDestroy(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - // Mandate a PUT request if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } args := structs.SessionRequest{ @@ -136,10 +132,8 @@ func (s *HTTPServer) SessionDestroy(resp http.ResponseWriter, req *http.Request) // SessionRenew is used to renew the TTL on an existing TTL session func (s *HTTPServer) SessionRenew(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - // Mandate a PUT request if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } args := structs.SessionSpecificRequest{} @@ -169,6 +163,10 @@ func (s *HTTPServer) SessionRenew(resp http.ResponseWriter, req *http.Request) ( // SessionGet is used to get info for a particular session func (s *HTTPServer) SessionGet(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + args := structs.SessionSpecificRequest{} if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { return nil, nil @@ -197,6 +195,10 @@ func (s *HTTPServer) SessionGet(resp http.ResponseWriter, req *http.Request) (in // SessionList is used to list all the sessions func (s *HTTPServer) SessionList(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + args := structs.DCSpecificRequest{} if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { return nil, nil @@ -217,6 +219,10 @@ func (s *HTTPServer) SessionList(resp http.ResponseWriter, req *http.Request) (i // SessionsForNode returns all the nodes belonging to a node func (s *HTTPServer) SessionsForNode(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + args := structs.NodeSpecificRequest{} if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { return nil, nil diff --git a/agent/snapshot_endpoint.go b/agent/snapshot_endpoint.go index b5bd451607..7c1bed366d 100644 --- a/agent/snapshot_endpoint.go +++ b/agent/snapshot_endpoint.go @@ -44,7 +44,6 @@ func (s *HTTPServer) Snapshot(resp http.ResponseWriter, req *http.Request) (inte return nil, nil default: - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT"}} } } diff --git a/agent/snapshot_endpoint_test.go b/agent/snapshot_endpoint_test.go index 81df2cfd06..ac3b515acb 100644 --- a/agent/snapshot_endpoint_test.go +++ b/agent/snapshot_endpoint_test.go @@ -101,38 +101,3 @@ func TestSnapshot_Options(t *testing.T) { }) } } - -func TestSnapshot_BadMethods(t *testing.T) { - t.Parallel() - t.Run("", func(t *testing.T) { - a := NewTestAgent(t.Name(), "") - defer a.Shutdown() - - body := bytes.NewBuffer(nil) - req, _ := http.NewRequest("POST", "/v1/snapshot", body) - resp := httptest.NewRecorder() - _, err := a.srv.Snapshot(resp, req) - if err != nil { - t.Fatalf("err: %v", err) - } - if resp.Code != 405 { - t.Fatalf("bad code: %d", resp.Code) - } - }) - - t.Run("", func(t *testing.T) { - a := NewTestAgent(t.Name(), "") - defer a.Shutdown() - - body := bytes.NewBuffer(nil) - req, _ := http.NewRequest("DELETE", "/v1/snapshot", body) - resp := httptest.NewRecorder() - _, err := a.srv.Snapshot(resp, req) - if err != nil { - t.Fatalf("err: %v", err) - } - if resp.Code != 405 { - t.Fatalf("bad code: %d", resp.Code) - } - }) -} diff --git a/agent/status_endpoint.go b/agent/status_endpoint.go index 75275800fd..2e3748267e 100644 --- a/agent/status_endpoint.go +++ b/agent/status_endpoint.go @@ -5,6 +5,10 @@ import ( ) func (s *HTTPServer) StatusLeader(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + var out string if err := s.agent.RPC("Status.Leader", struct{}{}, &out); err != nil { return nil, err @@ -13,6 +17,10 @@ func (s *HTTPServer) StatusLeader(resp http.ResponseWriter, req *http.Request) ( } func (s *HTTPServer) StatusPeers(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + var out []string if err := s.agent.RPC("Status.Peers", struct{}{}, &out); err != nil { return nil, err diff --git a/agent/status_endpoint_test.go b/agent/status_endpoint_test.go index ffe2f1be1a..79d1662b5b 100644 --- a/agent/status_endpoint_test.go +++ b/agent/status_endpoint_test.go @@ -1,13 +1,17 @@ package agent -import "testing" +import ( + "net/http" + "testing" +) func TestStatusLeader(t *testing.T) { t.Parallel() a := NewTestAgent(t.Name(), "") defer a.Shutdown() - obj, err := a.srv.StatusLeader(nil, nil) + req, _ := http.NewRequest("GET", "/v1/status/leader", nil) + obj, err := a.srv.StatusLeader(nil, req) if err != nil { t.Fatalf("Err: %v", err) } @@ -22,7 +26,8 @@ func TestStatusPeers(t *testing.T) { a := NewTestAgent(t.Name(), "") defer a.Shutdown() - obj, err := a.srv.StatusPeers(nil, nil) + req, _ := http.NewRequest("GET", "/v1/status/peers", nil) + obj, err := a.srv.StatusPeers(nil, req) if err != nil { t.Fatalf("Err: %v", err) } diff --git a/agent/testagent.go b/agent/testagent.go index 93ae334474..4998587fc8 100644 --- a/agent/testagent.go +++ b/agent/testagent.go @@ -15,6 +15,7 @@ import ( "strings" "time" + metrics "github.com/armon/go-metrics" "github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/agent/structs" @@ -147,6 +148,7 @@ func (a *TestAgent) Start() *TestAgent { agent.LogOutput = logOutput agent.LogWriter = a.LogWriter agent.logger = log.New(logOutput, a.Name+" - ", log.LstdFlags|log.Lmicroseconds) + agent.MemSink = &metrics.InmemSink{} // we need the err var in the next exit condition if err := agent.Start(); err == nil { diff --git a/agent/txn_endpoint.go b/agent/txn_endpoint.go index b38bd2d062..2d0b305749 100644 --- a/agent/txn_endpoint.go +++ b/agent/txn_endpoint.go @@ -173,8 +173,7 @@ func (s *HTTPServer) convertOps(resp http.ResponseWriter, req *http.Request) (st // and everything else will be routed through Raft like a normal write. func (s *HTTPServer) Txn(resp http.ResponseWriter, req *http.Request) (interface{}, error) { if req.Method != "PUT" { - resp.WriteHeader(http.StatusMethodNotAllowed) - return nil, nil + return nil, MethodNotAllowedError{req.Method, []string{"PUT"}} } // Convert the ops from the API format to the internal format. diff --git a/agent/txn_endpoint_test.go b/agent/txn_endpoint_test.go index faf653ff9e..35ec3f44ac 100644 --- a/agent/txn_endpoint_test.go +++ b/agent/txn_endpoint_test.go @@ -31,22 +31,6 @@ func TestTxnEndpoint_Bad_JSON(t *testing.T) { } } -func TestTxnEndpoint_Bad_Method(t *testing.T) { - t.Parallel() - a := NewTestAgent(t.Name(), "") - defer a.Shutdown() - - buf := bytes.NewBuffer([]byte("{}")) - req, _ := http.NewRequest("GET", "/v1/txn", buf) - resp := httptest.NewRecorder() - if _, err := a.srv.Txn(resp, req); err != nil { - t.Fatalf("err: %v", err) - } - if resp.Code != 405 { - t.Fatalf("expected 405, got %d", resp.Code) - } -} - func TestTxnEndpoint_Bad_Size_Item(t *testing.T) { t.Parallel() a := NewTestAgent(t.Name(), "") diff --git a/agent/ui_endpoint.go b/agent/ui_endpoint.go index e13f3ebb65..e43935131c 100644 --- a/agent/ui_endpoint.go +++ b/agent/ui_endpoint.go @@ -22,6 +22,10 @@ type ServiceSummary struct { // UINodes is used to list the nodes in a given datacenter. We return a // NodeDump which provides overview information for all the nodes func (s *HTTPServer) UINodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Parse arguments args := structs.DCSpecificRequest{} if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { @@ -59,6 +63,10 @@ RPC: // UINodeInfo is used to get info on a single node in a given datacenter. We return a // NodeInfo which provides overview information for the node func (s *HTTPServer) UINodeInfo(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Parse arguments args := structs.NodeSpecificRequest{} if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { @@ -105,6 +113,10 @@ RPC: // UIServices is used to list the services in a given datacenter. We return a // ServiceSummary which provides overview information for the service func (s *HTTPServer) UIServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Parse arguments args := structs.DCSpecificRequest{} if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { diff --git a/testutil/server_methods.go b/testutil/server_methods.go index 8f4b067ad6..dec512054c 100644 --- a/testutil/server_methods.go +++ b/testutil/server_methods.go @@ -25,13 +25,13 @@ const ( // JoinLAN is used to join local datacenters together. func (s *TestServer) JoinLAN(t *testing.T, addr string) { - resp := s.get(t, "/v1/agent/join/"+addr) + resp := s.put(t, "/v1/agent/join/"+addr, nil) defer resp.Body.Close() } // JoinWAN is used to join remote datacenters together. func (s *TestServer) JoinWAN(t *testing.T, addr string) { - resp := s.get(t, "/v1/agent/join/"+addr+"?wan=1") + resp := s.put(t, "/v1/agent/join/"+addr+"?wan=1", nil) resp.Body.Close() } diff --git a/website/source/api/agent.html.md b/website/source/api/agent.html.md index 1993e1caed..faf409f538 100644 --- a/website/source/api/agent.html.md +++ b/website/source/api/agent.html.md @@ -423,7 +423,7 @@ This endpoint instructs the agent to attempt to connect to a given address. | Method | Path | Produces | | ------ | ---------------------------- | -------------------------- | -| `GET` | `/agent/join/:address` | `application/json` | +| `PUT` | `/agent/join/:address` | `application/json` | The table below shows this endpoint's support for [blocking queries](/api/index.html#blocking-queries), diff --git a/website/source/api/agent/check.html.md b/website/source/api/agent/check.html.md index c95baf9375..16f1ec9700 100644 --- a/website/source/api/agent/check.html.md +++ b/website/source/api/agent/check.html.md @@ -214,7 +214,7 @@ This endpoint is used with a TTL type check to set the status of the check to | Method | Path | Produces | | ------ | ----------------------------- | -------------------------- | -| `GET` | `/agent/check/pass/:check_id` | `application/json` | +| `PUT` | `/agent/check/pass/:check_id` | `application/json` | The table below shows this endpoint's support for [blocking queries](/api/index.html#blocking-queries), @@ -247,7 +247,7 @@ This endpoint is used with a TTL type check to set the status of the check to | Method | Path | Produces | | ------ | ----------------------------- | -------------------------- | -| `GET` | `/agent/check/warn/:check_id` | `application/json` | +| `PUT` | `/agent/check/warn/:check_id` | `application/json` | The table below shows this endpoint's support for [blocking queries](/api/index.html#blocking-queries), @@ -280,7 +280,7 @@ This endpoint is used with a TTL type check to set the status of the check to | Method | Path | Produces | | ------ | ----------------------------- | -------------------------- | -| `GET` | `/agent/check/fail/:check_id` | `application/json` | +| `PUT` | `/agent/check/fail/:check_id` | `application/json` | The table below shows this endpoint's support for [blocking queries](/api/index.html#blocking-queries),