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

     1  package agent
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"strings"
     7  
     8  	"github.com/hashicorp/consul/agent/consul"
     9  	"github.com/hashicorp/consul/agent/structs"
    10  )
    11  
    12  // /v1/connection/intentions
    13  func (s *HTTPServer) IntentionEndpoint(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    14  	switch req.Method {
    15  	case "GET":
    16  		return s.IntentionList(resp, req)
    17  
    18  	case "POST":
    19  		return s.IntentionCreate(resp, req)
    20  
    21  	default:
    22  		return nil, MethodNotAllowedError{req.Method, []string{"GET", "POST"}}
    23  	}
    24  }
    25  
    26  // GET /v1/connect/intentions
    27  func (s *HTTPServer) IntentionList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    28  	// Method is tested in IntentionEndpoint
    29  
    30  	var args structs.DCSpecificRequest
    31  	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
    32  		return nil, nil
    33  	}
    34  
    35  	var reply structs.IndexedIntentions
    36  	defer setMeta(resp, &reply.QueryMeta)
    37  	if err := s.agent.RPC("Intention.List", &args, &reply); err != nil {
    38  		return nil, err
    39  	}
    40  
    41  	return reply.Intentions, nil
    42  }
    43  
    44  // POST /v1/connect/intentions
    45  func (s *HTTPServer) IntentionCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    46  	// Method is tested in IntentionEndpoint
    47  
    48  	args := structs.IntentionRequest{
    49  		Op: structs.IntentionOpCreate,
    50  	}
    51  	s.parseDC(req, &args.Datacenter)
    52  	s.parseToken(req, &args.Token)
    53  	if err := decodeBody(req, &args.Intention, nil); err != nil {
    54  		return nil, fmt.Errorf("Failed to decode request body: %s", err)
    55  	}
    56  
    57  	var reply string
    58  	if err := s.agent.RPC("Intention.Apply", &args, &reply); err != nil {
    59  		return nil, err
    60  	}
    61  
    62  	return intentionCreateResponse{reply}, nil
    63  }
    64  
    65  // GET /v1/connect/intentions/match
    66  func (s *HTTPServer) IntentionMatch(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    67  	// Prepare args
    68  	args := &structs.IntentionQueryRequest{Match: &structs.IntentionQueryMatch{}}
    69  	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
    70  		return nil, nil
    71  	}
    72  
    73  	q := req.URL.Query()
    74  
    75  	// Extract the "by" query parameter
    76  	if by, ok := q["by"]; !ok || len(by) != 1 {
    77  		return nil, fmt.Errorf("required query parameter 'by' not set")
    78  	} else {
    79  		switch v := structs.IntentionMatchType(by[0]); v {
    80  		case structs.IntentionMatchSource, structs.IntentionMatchDestination:
    81  			args.Match.Type = v
    82  		default:
    83  			return nil, fmt.Errorf("'by' parameter must be one of 'source' or 'destination'")
    84  		}
    85  	}
    86  
    87  	// Extract all the match names
    88  	names, ok := q["name"]
    89  	if !ok || len(names) == 0 {
    90  		return nil, fmt.Errorf("required query parameter 'name' not set")
    91  	}
    92  
    93  	// Build the entries in order. The order matters since that is the
    94  	// order of the returned responses.
    95  	args.Match.Entries = make([]structs.IntentionMatchEntry, len(names))
    96  	for i, n := range names {
    97  		entry, err := parseIntentionMatchEntry(n)
    98  		if err != nil {
    99  			return nil, fmt.Errorf("name %q is invalid: %s", n, err)
   100  		}
   101  
   102  		args.Match.Entries[i] = entry
   103  	}
   104  
   105  	var reply structs.IndexedIntentionMatches
   106  	if err := s.agent.RPC("Intention.Match", args, &reply); err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	// We must have an identical count of matches
   111  	if len(reply.Matches) != len(names) {
   112  		return nil, fmt.Errorf("internal error: match response count didn't match input count")
   113  	}
   114  
   115  	// Use empty list instead of nil.
   116  	response := make(map[string]structs.Intentions)
   117  	for i, ixns := range reply.Matches {
   118  		response[names[i]] = ixns
   119  	}
   120  
   121  	return response, nil
   122  }
   123  
   124  // GET /v1/connect/intentions/check
   125  func (s *HTTPServer) IntentionCheck(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   126  	// Prepare args
   127  	args := &structs.IntentionQueryRequest{Check: &structs.IntentionQueryCheck{}}
   128  	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
   129  		return nil, nil
   130  	}
   131  
   132  	q := req.URL.Query()
   133  
   134  	// Set the source type if set
   135  	args.Check.SourceType = structs.IntentionSourceConsul
   136  	if sourceType, ok := q["source-type"]; ok && len(sourceType) > 0 {
   137  		args.Check.SourceType = structs.IntentionSourceType(sourceType[0])
   138  	}
   139  
   140  	// Extract the source/destination
   141  	source, ok := q["source"]
   142  	if !ok || len(source) != 1 {
   143  		return nil, fmt.Errorf("required query parameter 'source' not set")
   144  	}
   145  	destination, ok := q["destination"]
   146  	if !ok || len(destination) != 1 {
   147  		return nil, fmt.Errorf("required query parameter 'destination' not set")
   148  	}
   149  
   150  	// We parse them the same way as matches to extract namespace/name
   151  	args.Check.SourceName = source[0]
   152  	if args.Check.SourceType == structs.IntentionSourceConsul {
   153  		entry, err := parseIntentionMatchEntry(source[0])
   154  		if err != nil {
   155  			return nil, fmt.Errorf("source %q is invalid: %s", source[0], err)
   156  		}
   157  		args.Check.SourceNS = entry.Namespace
   158  		args.Check.SourceName = entry.Name
   159  	}
   160  
   161  	// The destination is always in the Consul format
   162  	entry, err := parseIntentionMatchEntry(destination[0])
   163  	if err != nil {
   164  		return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err)
   165  	}
   166  	args.Check.DestinationNS = entry.Namespace
   167  	args.Check.DestinationName = entry.Name
   168  
   169  	var reply structs.IntentionQueryCheckResponse
   170  	if err := s.agent.RPC("Intention.Check", args, &reply); err != nil {
   171  		return nil, err
   172  	}
   173  
   174  	return &reply, nil
   175  }
   176  
   177  // IntentionSpecific handles the endpoint for /v1/connection/intentions/:id
   178  func (s *HTTPServer) IntentionSpecific(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   179  	id := strings.TrimPrefix(req.URL.Path, "/v1/connect/intentions/")
   180  
   181  	switch req.Method {
   182  	case "GET":
   183  		return s.IntentionSpecificGet(id, resp, req)
   184  
   185  	case "PUT":
   186  		return s.IntentionSpecificUpdate(id, resp, req)
   187  
   188  	case "DELETE":
   189  		return s.IntentionSpecificDelete(id, resp, req)
   190  
   191  	default:
   192  		return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}}
   193  	}
   194  }
   195  
   196  // GET /v1/connect/intentions/:id
   197  func (s *HTTPServer) IntentionSpecificGet(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   198  	// Method is tested in IntentionEndpoint
   199  
   200  	args := structs.IntentionQueryRequest{
   201  		IntentionID: id,
   202  	}
   203  	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
   204  		return nil, nil
   205  	}
   206  
   207  	var reply structs.IndexedIntentions
   208  	if err := s.agent.RPC("Intention.Get", &args, &reply); err != nil {
   209  		// We have to check the string since the RPC sheds the error type
   210  		if err.Error() == consul.ErrIntentionNotFound.Error() {
   211  			resp.WriteHeader(http.StatusNotFound)
   212  			fmt.Fprint(resp, err.Error())
   213  			return nil, nil
   214  		}
   215  
   216  		// Not ideal, but there are a number of error scenarios that are not
   217  		// user error (400). We look for a specific case of invalid UUID
   218  		// to detect a parameter error and return a 400 response. The error
   219  		// is not a constant type or message, so we have to use strings.Contains
   220  		if strings.Contains(err.Error(), "UUID") {
   221  			return nil, BadRequestError{Reason: err.Error()}
   222  		}
   223  
   224  		return nil, err
   225  	}
   226  
   227  	// This shouldn't happen since the called API documents it shouldn't,
   228  	// but we check since the alternative if it happens is a panic.
   229  	if len(reply.Intentions) == 0 {
   230  		resp.WriteHeader(http.StatusNotFound)
   231  		return nil, nil
   232  	}
   233  
   234  	return reply.Intentions[0], nil
   235  }
   236  
   237  // PUT /v1/connect/intentions/:id
   238  func (s *HTTPServer) IntentionSpecificUpdate(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   239  	// Method is tested in IntentionEndpoint
   240  
   241  	args := structs.IntentionRequest{
   242  		Op: structs.IntentionOpUpdate,
   243  	}
   244  	s.parseDC(req, &args.Datacenter)
   245  	s.parseToken(req, &args.Token)
   246  	if err := decodeBody(req, &args.Intention, nil); err != nil {
   247  		resp.WriteHeader(http.StatusBadRequest)
   248  		fmt.Fprintf(resp, "Request decode failed: %v", err)
   249  		return nil, nil
   250  	}
   251  
   252  	// Use the ID from the URL
   253  	args.Intention.ID = id
   254  
   255  	var reply string
   256  	if err := s.agent.RPC("Intention.Apply", &args, &reply); err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	// Update uses the same create response
   261  	return intentionCreateResponse{reply}, nil
   262  
   263  }
   264  
   265  // DELETE /v1/connect/intentions/:id
   266  func (s *HTTPServer) IntentionSpecificDelete(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   267  	// Method is tested in IntentionEndpoint
   268  
   269  	args := structs.IntentionRequest{
   270  		Op:        structs.IntentionOpDelete,
   271  		Intention: &structs.Intention{ID: id},
   272  	}
   273  	s.parseDC(req, &args.Datacenter)
   274  	s.parseToken(req, &args.Token)
   275  
   276  	var reply string
   277  	if err := s.agent.RPC("Intention.Apply", &args, &reply); err != nil {
   278  		return nil, err
   279  	}
   280  
   281  	return true, nil
   282  }
   283  
   284  // intentionCreateResponse is the response structure for creating an intention.
   285  type intentionCreateResponse struct{ ID string }
   286  
   287  // parseIntentionMatchEntry parses the query parameter for an intention
   288  // match query entry.
   289  func parseIntentionMatchEntry(input string) (structs.IntentionMatchEntry, error) {
   290  	var result structs.IntentionMatchEntry
   291  	result.Namespace = structs.IntentionDefaultNamespace
   292  
   293  	// TODO(mitchellh): when namespaces are introduced, set the default
   294  	// namespace to be the namespace of the requestor.
   295  
   296  	// Get the index to the '/'. If it doesn't exist, we have just a name
   297  	// so just set that and return.
   298  	idx := strings.IndexByte(input, '/')
   299  	if idx == -1 {
   300  		result.Name = input
   301  		return result, nil
   302  	}
   303  
   304  	result.Namespace = input[:idx]
   305  	result.Name = input[idx+1:]
   306  	if strings.IndexByte(result.Name, '/') != -1 {
   307  		return result, fmt.Errorf("input can contain at most one '/'")
   308  	}
   309  
   310  	return result, nil
   311  }