github.com/KyaXTeam/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 }