1package cfclient 2 3import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "reflect" 10 "strings" 11 12 "github.com/Masterminds/semver" 13 "github.com/pkg/errors" 14) 15 16type SecGroupResponse struct { 17 Count int `json:"total_results"` 18 Pages int `json:"total_pages"` 19 NextUrl string `json:"next_url"` 20 Resources []SecGroupResource `json:"resources"` 21} 22 23type SecGroupCreateResponse struct { 24 Code int `json:"code"` 25 ErrorCode string `json:"error_code"` 26 Description string `json:"description"` 27} 28 29type SecGroupResource struct { 30 Meta Meta `json:"metadata"` 31 Entity SecGroup `json:"entity"` 32} 33 34type SecGroup struct { 35 Guid string `json:"guid"` 36 Name string `json:"name"` 37 CreatedAt string `json:"created_at"` 38 UpdatedAt string `json:"updated_at"` 39 Rules []SecGroupRule `json:"rules"` 40 Running bool `json:"running_default"` 41 Staging bool `json:"staging_default"` 42 SpacesURL string `json:"spaces_url"` 43 StagingSpacesURL string `json:"staging_spaces_url"` 44 SpacesData []SpaceResource `json:"spaces"` 45 StagingSpacesData []SpaceResource `json:"staging_spaces"` 46 c *Client 47} 48 49type SecGroupRule struct { 50 Protocol string `json:"protocol"` 51 Ports string `json:"ports,omitempty"` //e.g. "4000-5000,9142" 52 Destination string `json:"destination"` //CIDR Format 53 Description string `json:"description,omitempty"` //Optional description 54 Code int `json:"code"` // ICMP code 55 Type int `json:"type"` //ICMP type. Only valid if Protocol=="icmp" 56 Log bool `json:"log,omitempty"` //If true, log this rule 57} 58 59var MinStagingSpacesVersion *semver.Version = getMinStagingSpacesVersion() 60 61func (c *Client) ListSecGroups() (secGroups []SecGroup, err error) { 62 requestURL := "/v2/security_groups?inline-relations-depth=1" 63 for requestURL != "" { 64 var secGroupResp SecGroupResponse 65 r := c.NewRequest("GET", requestURL) 66 resp, err := c.DoRequest(r) 67 68 if err != nil { 69 return nil, errors.Wrap(err, "Error requesting sec groups") 70 } 71 resBody, err := ioutil.ReadAll(resp.Body) 72 if err != nil { 73 return nil, errors.Wrap(err, "Error reading sec group response body") 74 } 75 76 err = json.Unmarshal(resBody, &secGroupResp) 77 if err != nil { 78 return nil, errors.Wrap(err, "Error unmarshaling sec group") 79 } 80 81 for _, secGroup := range secGroupResp.Resources { 82 secGroup.Entity.Guid = secGroup.Meta.Guid 83 secGroup.Entity.CreatedAt = secGroup.Meta.CreatedAt 84 secGroup.Entity.UpdatedAt = secGroup.Meta.UpdatedAt 85 secGroup.Entity.c = c 86 for i, space := range secGroup.Entity.SpacesData { 87 space.Entity.Guid = space.Meta.Guid 88 secGroup.Entity.SpacesData[i] = space 89 } 90 if len(secGroup.Entity.SpacesData) == 0 { 91 spaces, err := secGroup.Entity.ListSpaceResources() 92 if err != nil { 93 return nil, err 94 } 95 for _, space := range spaces { 96 secGroup.Entity.SpacesData = append(secGroup.Entity.SpacesData, space) 97 } 98 } 99 if len(secGroup.Entity.StagingSpacesData) == 0 { 100 spaces, err := secGroup.Entity.ListStagingSpaceResources() 101 if err != nil { 102 return nil, err 103 } 104 for _, space := range spaces { 105 secGroup.Entity.StagingSpacesData = append(secGroup.Entity.SpacesData, space) 106 } 107 } 108 secGroups = append(secGroups, secGroup.Entity) 109 } 110 111 requestURL = secGroupResp.NextUrl 112 resp.Body.Close() 113 } 114 return secGroups, nil 115} 116 117func (c *Client) ListRunningSecGroups() ([]SecGroup, error) { 118 secGroups := make([]SecGroup, 0) 119 requestURL := "/v2/config/running_security_groups" 120 for requestURL != "" { 121 var secGroupResp SecGroupResponse 122 r := c.NewRequest("GET", requestURL) 123 resp, err := c.DoRequest(r) 124 125 if err != nil { 126 return nil, errors.Wrap(err, "Error requesting sec groups") 127 } 128 resBody, err := ioutil.ReadAll(resp.Body) 129 if err != nil { 130 return nil, errors.Wrap(err, "Error reading sec group response body") 131 } 132 133 err = json.Unmarshal(resBody, &secGroupResp) 134 if err != nil { 135 return nil, errors.Wrap(err, "Error unmarshaling sec group") 136 } 137 138 for _, secGroup := range secGroupResp.Resources { 139 secGroup.Entity.Guid = secGroup.Meta.Guid 140 secGroup.Entity.CreatedAt = secGroup.Meta.CreatedAt 141 secGroup.Entity.UpdatedAt = secGroup.Meta.UpdatedAt 142 secGroup.Entity.c = c 143 144 secGroups = append(secGroups, secGroup.Entity) 145 } 146 147 requestURL = secGroupResp.NextUrl 148 resp.Body.Close() 149 } 150 return secGroups, nil 151} 152 153func (c *Client) ListStagingSecGroups() ([]SecGroup, error) { 154 secGroups := make([]SecGroup, 0) 155 requestURL := "/v2/config/staging_security_groups" 156 for requestURL != "" { 157 var secGroupResp SecGroupResponse 158 r := c.NewRequest("GET", requestURL) 159 resp, err := c.DoRequest(r) 160 161 if err != nil { 162 return nil, errors.Wrap(err, "Error requesting sec groups") 163 } 164 resBody, err := ioutil.ReadAll(resp.Body) 165 if err != nil { 166 return nil, errors.Wrap(err, "Error reading sec group response body") 167 } 168 169 err = json.Unmarshal(resBody, &secGroupResp) 170 if err != nil { 171 return nil, errors.Wrap(err, "Error unmarshaling sec group") 172 } 173 174 for _, secGroup := range secGroupResp.Resources { 175 secGroup.Entity.Guid = secGroup.Meta.Guid 176 secGroup.Entity.CreatedAt = secGroup.Meta.CreatedAt 177 secGroup.Entity.UpdatedAt = secGroup.Meta.UpdatedAt 178 secGroup.Entity.c = c 179 180 secGroups = append(secGroups, secGroup.Entity) 181 } 182 183 requestURL = secGroupResp.NextUrl 184 resp.Body.Close() 185 } 186 return secGroups, nil 187} 188 189func (c *Client) GetSecGroupByName(name string) (secGroup SecGroup, err error) { 190 requestURL := "/v2/security_groups?q=name:" + name 191 var secGroupResp SecGroupResponse 192 r := c.NewRequest("GET", requestURL) 193 resp, err := c.DoRequest(r) 194 195 if err != nil { 196 return secGroup, errors.Wrap(err, "Error requesting sec groups") 197 } 198 resBody, err := ioutil.ReadAll(resp.Body) 199 if err != nil { 200 return secGroup, errors.Wrap(err, "Error reading sec group response body") 201 } 202 203 err = json.Unmarshal(resBody, &secGroupResp) 204 if err != nil { 205 return secGroup, errors.Wrap(err, "Error unmarshaling sec group") 206 } 207 if len(secGroupResp.Resources) == 0 { 208 return secGroup, fmt.Errorf("No security group with name %v found", name) 209 } 210 secGroup = secGroupResp.Resources[0].Entity 211 secGroup.Guid = secGroupResp.Resources[0].Meta.Guid 212 secGroup.CreatedAt = secGroupResp.Resources[0].Meta.CreatedAt 213 secGroup.UpdatedAt = secGroupResp.Resources[0].Meta.UpdatedAt 214 secGroup.c = c 215 216 resp.Body.Close() 217 return secGroup, nil 218} 219 220func (secGroup *SecGroup) ListSpaceResources() ([]SpaceResource, error) { 221 var spaceResources []SpaceResource 222 requestURL := secGroup.SpacesURL 223 for requestURL != "" { 224 spaceResp, err := secGroup.c.getSpaceResponse(requestURL) 225 if err != nil { 226 return []SpaceResource{}, err 227 } 228 for i, spaceRes := range spaceResp.Resources { 229 spaceRes.Entity.Guid = spaceRes.Meta.Guid 230 spaceRes.Entity.CreatedAt = spaceRes.Meta.CreatedAt 231 spaceRes.Entity.UpdatedAt = spaceRes.Meta.UpdatedAt 232 spaceResp.Resources[i] = spaceRes 233 } 234 spaceResources = append(spaceResources, spaceResp.Resources...) 235 requestURL = spaceResp.NextUrl 236 } 237 return spaceResources, nil 238} 239 240func (secGroup *SecGroup) ListStagingSpaceResources() ([]SpaceResource, error) { 241 var spaceResources []SpaceResource 242 requestURL := secGroup.StagingSpacesURL 243 for requestURL != "" { 244 spaceResp, err := secGroup.c.getSpaceResponse(requestURL) 245 if err != nil { 246 // if this is a 404, let's make sure that it's not because we're on a legacy system 247 if cause := errors.Cause(err); cause != nil { 248 if httpErr, ok := cause.(CloudFoundryHTTPError); ok { 249 if httpErr.StatusCode == 404 { 250 info, infoErr := secGroup.c.GetInfo() 251 if infoErr != nil { 252 return nil, infoErr 253 } 254 255 apiVersion, versionErr := semver.NewVersion(info.APIVersion) 256 if versionErr != nil { 257 return nil, versionErr 258 } 259 260 if MinStagingSpacesVersion.GreaterThan(apiVersion) { 261 // this is probably not really an error, we're just trying to use a non-existent api 262 return nil, nil 263 } 264 } 265 } 266 } 267 268 return []SpaceResource{}, err 269 } 270 for i, spaceRes := range spaceResp.Resources { 271 spaceRes.Entity.Guid = spaceRes.Meta.Guid 272 spaceRes.Entity.CreatedAt = spaceRes.Meta.CreatedAt 273 spaceRes.Entity.UpdatedAt = spaceRes.Meta.UpdatedAt 274 spaceResp.Resources[i] = spaceRes 275 } 276 spaceResources = append(spaceResources, spaceResp.Resources...) 277 requestURL = spaceResp.NextUrl 278 } 279 return spaceResources, nil 280} 281 282/* 283CreateSecGroup contacts the CF endpoint for creating a new security group. 284name: the name to give to the created security group 285rules: A slice of rule objects that describe the rules that this security group enforces. 286 This can technically be nil or an empty slice - we won't judge you 287spaceGuids: The security group will be associated with the spaces specified by the contents of this slice. 288 If nil, the security group will not be associated with any spaces initially. 289*/ 290func (c *Client) CreateSecGroup(name string, rules []SecGroupRule, spaceGuids []string) (*SecGroup, error) { 291 return c.secGroupCreateHelper("/v2/security_groups", "POST", name, rules, spaceGuids) 292} 293 294/* 295UpdateSecGroup contacts the CF endpoint to update an existing security group. 296guid: identifies the security group that you would like to update. 297name: the new name to give to the security group 298rules: A slice of rule objects that describe the rules that this security group enforces. 299 If this is left nil, the rules will not be changed. 300spaceGuids: The security group will be associated with the spaces specified by the contents of this slice. 301 If nil, the space associations will not be changed. 302*/ 303func (c *Client) UpdateSecGroup(guid, name string, rules []SecGroupRule, spaceGuids []string) (*SecGroup, error) { 304 return c.secGroupCreateHelper("/v2/security_groups/"+guid, "PUT", name, rules, spaceGuids) 305} 306 307/* 308DeleteSecGroup contacts the CF endpoint to delete an existing security group. 309guid: Indentifies the security group to be deleted. 310*/ 311func (c *Client) DeleteSecGroup(guid string) error { 312 //Perform the DELETE and check for errors 313 resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/security_groups/%s", guid))) 314 if err != nil { 315 return err 316 } 317 if resp.StatusCode != 204 { //204 No Content 318 return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 319 } 320 return nil 321} 322 323/* 324GetSecGroup contacts the CF endpoint for fetching the info for a particular security group. 325guid: Identifies the security group to fetch information from 326*/ 327func (c *Client) GetSecGroup(guid string) (*SecGroup, error) { 328 //Perform the GET and check for errors 329 resp, err := c.DoRequest(c.NewRequest("GET", "/v2/security_groups/"+guid)) 330 if err != nil { 331 return nil, err 332 } 333 if resp.StatusCode != 200 { 334 return nil, fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 335 } 336 //get the json out of the response body 337 return respBodyToSecGroup(resp.Body, c) 338} 339 340/* 341BindSecGroup contacts the CF endpoint to associate a space with a security group 342secGUID: identifies the security group to add a space to 343spaceGUID: identifies the space to associate 344*/ 345func (c *Client) BindSecGroup(secGUID, spaceGUID string) error { 346 //Perform the PUT and check for errors 347 resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/security_groups/%s/spaces/%s", secGUID, spaceGUID))) 348 if err != nil { 349 return err 350 } 351 if resp.StatusCode != 201 { //201 Created 352 return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 353 } 354 return nil 355} 356 357/* 358BindSpaceStagingSecGroup contacts the CF endpoint to associate a space with a security group for staging functions only 359secGUID: identifies the security group to add a space to 360spaceGUID: identifies the space to associate 361*/ 362func (c *Client) BindStagingSecGroupToSpace(secGUID, spaceGUID string) error { 363 //Perform the PUT and check for errors 364 resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/security_groups/%s/staging_spaces/%s", secGUID, spaceGUID))) 365 if err != nil { 366 return err 367 } 368 if resp.StatusCode != 201 { //201 Created 369 return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 370 } 371 return nil 372} 373 374/* 375BindRunningSecGroup contacts the CF endpoint to associate a security group 376secGUID: identifies the security group to add a space to 377*/ 378func (c *Client) BindRunningSecGroup(secGUID string) error { 379 //Perform the PUT and check for errors 380 resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/config/running_security_groups/%s", secGUID))) 381 if err != nil { 382 return err 383 } 384 if resp.StatusCode != 200 { //200 385 return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 386 } 387 return nil 388} 389 390/* 391UnbindRunningSecGroup contacts the CF endpoint to dis-associate a security group 392secGUID: identifies the security group to add a space to 393*/ 394func (c *Client) UnbindRunningSecGroup(secGUID string) error { 395 //Perform the DELETE and check for errors 396 resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/config/running_security_groups/%s", secGUID))) 397 if err != nil { 398 return err 399 } 400 if resp.StatusCode != http.StatusNoContent { //204 401 return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 402 } 403 return nil 404} 405 406/* 407BindStagingSecGroup contacts the CF endpoint to associate a space with a security group 408secGUID: identifies the security group to add a space to 409*/ 410func (c *Client) BindStagingSecGroup(secGUID string) error { 411 //Perform the PUT and check for errors 412 resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/config/staging_security_groups/%s", secGUID))) 413 if err != nil { 414 return err 415 } 416 if resp.StatusCode != 200 { //200 417 return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 418 } 419 return nil 420} 421 422/* 423UnbindStagingSecGroup contacts the CF endpoint to dis-associate a space with a security group 424secGUID: identifies the security group to add a space to 425*/ 426func (c *Client) UnbindStagingSecGroup(secGUID string) error { 427 //Perform the DELETE and check for errors 428 resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/config/staging_security_groups/%s", secGUID))) 429 if err != nil { 430 return err 431 } 432 if resp.StatusCode != http.StatusNoContent { //204 433 return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 434 } 435 return nil 436} 437 438/* 439UnbindSecGroup contacts the CF endpoint to dissociate a space from a security group 440secGUID: identifies the security group to remove a space from 441spaceGUID: identifies the space to dissociate from the security group 442*/ 443func (c *Client) UnbindSecGroup(secGUID, spaceGUID string) error { 444 //Perform the DELETE and check for errors 445 resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/security_groups/%s/spaces/%s", secGUID, spaceGUID))) 446 if err != nil { 447 return err 448 } 449 if resp.StatusCode != 204 { //204 No Content 450 return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 451 } 452 return nil 453} 454 455//Reads most security group response bodies into a SecGroup object 456func respBodyToSecGroup(body io.ReadCloser, c *Client) (*SecGroup, error) { 457 //get the json from the response body 458 bodyRaw, err := ioutil.ReadAll(body) 459 if err != nil { 460 return nil, errors.Wrap(err, "Could not read response body") 461 } 462 jStruct := SecGroupResource{} 463 //make it a SecGroup 464 err = json.Unmarshal(bodyRaw, &jStruct) 465 if err != nil { 466 return nil, errors.Wrap(err, "Could not unmarshal response body as json") 467 } 468 //pull a few extra fields from other places 469 ret := jStruct.Entity 470 ret.Guid = jStruct.Meta.Guid 471 ret.CreatedAt = jStruct.Meta.CreatedAt 472 ret.UpdatedAt = jStruct.Meta.UpdatedAt 473 ret.c = c 474 return &ret, nil 475} 476 477func convertStructToMap(st interface{}) map[string]interface{} { 478 reqRules := make(map[string]interface{}) 479 480 v := reflect.ValueOf(st) 481 t := reflect.TypeOf(st) 482 483 for i := 0; i < v.NumField(); i++ { 484 key := strings.ToLower(t.Field(i).Name) 485 typ := v.FieldByName(t.Field(i).Name).Kind().String() 486 structTag := t.Field(i).Tag.Get("json") 487 jsonName := strings.TrimSpace(strings.Split(structTag, ",")[0]) 488 value := v.FieldByName(t.Field(i).Name) 489 490 // if jsonName is not empty use it for the key 491 if jsonName != "" { 492 key = jsonName 493 } 494 495 if typ == "string" { 496 if !(value.String() == "" && strings.Contains(structTag, "omitempty")) { 497 reqRules[key] = value.String() 498 } 499 } else if typ == "int" { 500 reqRules[key] = value.Int() 501 } else { 502 reqRules[key] = value.Interface() 503 } 504 505 } 506 507 return reqRules 508} 509 510//Create and Update secGroup pretty much do the same thing, so this function abstracts those out. 511func (c *Client) secGroupCreateHelper(url, method, name string, rules []SecGroupRule, spaceGuids []string) (*SecGroup, error) { 512 reqRules := make([]map[string]interface{}, len(rules)) 513 514 for i, rule := range rules { 515 reqRules[i] = convertStructToMap(rule) 516 protocol := strings.ToLower(reqRules[i]["protocol"].(string)) 517 518 // if not icmp protocol need to remove the Code/Type fields 519 if protocol != "icmp" { 520 delete(reqRules[i], "code") 521 delete(reqRules[i], "type") 522 } 523 } 524 525 req := c.NewRequest(method, url) 526 //set up request body 527 inputs := map[string]interface{}{ 528 "name": name, 529 "rules": reqRules, 530 } 531 532 if spaceGuids != nil { 533 inputs["space_guids"] = spaceGuids 534 } 535 req.obj = inputs 536 //fire off the request and check for problems 537 resp, err := c.DoRequest(req) 538 if err != nil { 539 return nil, err 540 } 541 if resp.StatusCode != 201 { // Both create and update should give 201 CREATED 542 var response SecGroupCreateResponse 543 544 bodyRaw, _ := ioutil.ReadAll(resp.Body) 545 546 err = json.Unmarshal(bodyRaw, &response) 547 if err != nil { 548 return nil, errors.Wrap(err, "Error unmarshaling response") 549 } 550 551 return nil, fmt.Errorf(`Request failed CF API returned with status code %d 552------------------------------- 553Error Code %s 554Code %d 555Description %s`, 556 resp.StatusCode, response.ErrorCode, response.Code, response.Description) 557 } 558 //get the json from the response body 559 return respBodyToSecGroup(resp.Body, c) 560} 561 562func getMinStagingSpacesVersion() *semver.Version { 563 v, _ := semver.NewVersion("2.68.0") 564 return v 565} 566