diff --git a/agent/consul/intention_endpoint.go b/agent/consul/intention_endpoint.go index 3313c03f7a..030c9922da 100644 --- a/agent/consul/intention_endpoint.go +++ b/agent/consul/intention_endpoint.go @@ -1,6 +1,7 @@ package consul import ( + "errors" "time" "github.com/armon/go-metrics" @@ -10,6 +11,11 @@ import ( "github.com/hashicorp/go-uuid" ) +var ( + // ErrIntentionNotFound is returned if the intention lookup failed. + ErrIntentionNotFound = errors.New("Intention not found") +) + // Intention manages the Connect intentions. type Intention struct { // srv is a pointer back to the server. @@ -83,7 +89,7 @@ func (s *Intention) Get( return err } if ixn == nil { - return ErrQueryNotFound + return ErrIntentionNotFound } reply.Index = index diff --git a/agent/http_oss.go b/agent/http_oss.go index 61bef8d2a9..0170a00750 100644 --- a/agent/http_oss.go +++ b/agent/http_oss.go @@ -39,7 +39,8 @@ func init() { registerEndpoint("/v1/catalog/services", []string{"GET"}, (*HTTPServer).CatalogServices) registerEndpoint("/v1/catalog/service/", []string{"GET"}, (*HTTPServer).CatalogServiceNodes) registerEndpoint("/v1/catalog/node/", []string{"GET"}, (*HTTPServer).CatalogNodeServices) - registerEndpoint("/v1/connect/intentions", []string{"GET", "POST"}, (*HTTPServer).IntentionList) + registerEndpoint("/v1/connect/intentions", []string{"GET", "POST"}, (*HTTPServer).IntentionEndpoint) + registerEndpoint("/v1/connect/intentions/", []string{"GET"}, (*HTTPServer).IntentionSpecific) registerEndpoint("/v1/coordinate/datacenters", []string{"GET"}, (*HTTPServer).CoordinateDatacenters) registerEndpoint("/v1/coordinate/nodes", []string{"GET"}, (*HTTPServer).CoordinateNodes) registerEndpoint("/v1/coordinate/node/", []string{"GET"}, (*HTTPServer).CoordinateNode) diff --git a/agent/intentions_endpoint.go b/agent/intentions_endpoint.go index 62340e7e71..d5d6b6495b 100644 --- a/agent/intentions_endpoint.go +++ b/agent/intentions_endpoint.go @@ -3,7 +3,9 @@ package agent import ( "fmt" "net/http" + "strings" + "github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/agent/structs" ) @@ -65,5 +67,51 @@ func (s *HTTPServer) IntentionCreate(resp http.ResponseWriter, req *http.Request return intentionCreateResponse{reply}, nil } +// IntentionSpecific handles the endpoint for /v1/connection/intentions/:id +func (s *HTTPServer) IntentionSpecific(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + id := strings.TrimPrefix(req.URL.Path, "/v1/connect/intentions/") + + switch req.Method { + case "GET": + return s.IntentionSpecificGet(id, resp, req) + + case "PUT": + panic("TODO") + + case "DELETE": + panic("TODO") + + default: + return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}} + } +} + +// GET /v1/connect/intentions/:id +func (s *HTTPServer) IntentionSpecificGet(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { + // Method is tested in IntentionEndpoint + + args := structs.IntentionQueryRequest{ + IntentionID: id, + } + if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + return nil, nil + } + + var reply structs.IndexedIntentions + if err := s.agent.RPC("Intention.Get", &args, &reply); err != nil { + // We have to check the string since the RPC sheds the error type + if err.Error() == consul.ErrIntentionNotFound.Error() { + resp.WriteHeader(http.StatusNotFound) + fmt.Fprint(resp, err.Error()) + return nil, nil + } + + return nil, err + } + + // TODO: validate length + return reply.Intentions[0], nil +} + // intentionCreateResponse is the response structure for creating an intention. type intentionCreateResponse struct{ ID string } diff --git a/agent/intentions_endpoint_test.go b/agent/intentions_endpoint_test.go index db6a16580b..0bd956842b 100644 --- a/agent/intentions_endpoint_test.go +++ b/agent/intentions_endpoint_test.go @@ -1,6 +1,7 @@ package agent import ( + "fmt" "net/http" "net/http/httptest" "reflect" @@ -109,3 +110,45 @@ func TestIntentionsCreate_good(t *testing.T) { } } } + +func TestIntentionsSpecificGet_good(t *testing.T) { + t.Parallel() + + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + // The intention + ixn := &structs.Intention{SourceName: "foo"} + + // Create an intention directly + var reply string + { + req := structs.IntentionRequest{ + Datacenter: "dc1", + Op: structs.IntentionOpCreate, + Intention: ixn, + } + if err := a.RPC("Intention.Apply", &req, &reply); err != nil { + t.Fatalf("err: %s", err) + } + } + + // Get the value + req, _ := http.NewRequest("GET", fmt.Sprintf("/v1/connect/intentions/%s", reply), nil) + resp := httptest.NewRecorder() + obj, err := a.srv.IntentionSpecific(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + value := obj.(*structs.Intention) + if value.ID != reply { + t.Fatalf("bad: %v", value) + } + + ixn.ID = value.ID + ixn.RaftIndex = value.RaftIndex + if !reflect.DeepEqual(value, ixn) { + t.Fatalf("bad (got, want):\n\n%#v\n\n%#v", value, ixn) + } +}