1package cfclient 2 3import ( 4 "bytes" 5 "crypto/tls" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "mime/multipart" 11 "net/http" 12 "net/url" 13 "os" 14 "strconv" 15 "strings" 16 "time" 17 18 "github.com/pkg/errors" 19) 20 21type AppResponse struct { 22 Count int `json:"total_results"` 23 Pages int `json:"total_pages"` 24 NextUrl string `json:"next_url"` 25 Resources []AppResource `json:"resources"` 26} 27 28type AppResource struct { 29 Meta Meta `json:"metadata"` 30 Entity App `json:"entity"` 31} 32 33type AppState string 34 35const ( 36 APP_STOPPED AppState = "STOPPED" 37 APP_STARTED AppState = "STARTED" 38) 39 40type HealthCheckType string 41 42const ( 43 HEALTH_HTTP HealthCheckType = "http" 44 HEALTH_PORT HealthCheckType = "port" 45 HEALTH_PROCESS HealthCheckType = "process" 46) 47 48type DockerCredentials struct { 49 Username string `json:"username,omitempty"` 50 Password string `json:"password,omitempty"` 51} 52 53type AppCreateRequest struct { 54 Name string `json:"name"` 55 SpaceGuid string `json:"space_guid"` 56 // Memory for the app, in MB 57 Memory int `json:"memory,omitempty"` 58 // Instances to startup 59 Instances int `json:"instances,omitempty"` 60 // Disk quota in MB 61 DiskQuota int `json:"disk_quota,omitempty"` 62 StackGuid string `json:"stack_guid,omitempty"` 63 // Desired state of the app. Either "STOPPED" or "STARTED" 64 State AppState `json:"state,omitempty"` 65 // Command to start an app 66 Command string `json:"command,omitempty"` 67 // Buildpack to build the app. Three options: 68 // 1. Blank for autodetection 69 // 2. GIT url 70 // 3. Name of an installed buildpack 71 Buildpack string `json:"buildpack,omitempty"` 72 // Endpoint to check if an app is healthy 73 HealthCheckHttpEndpoint string `json:"health_check_http_endpoint,omitempty"` 74 // How to check if an app is healthy. Defaults to HEALTH_PORT if not specified 75 HealthCheckType HealthCheckType `json:"health_check_type,omitempty"` 76 Diego bool `json:"diego,omitempty"` 77 EnableSSH bool `json:"enable_ssh,omitempty"` 78 DockerImage string `json:"docker_image,omitempty"` 79 DockerCredentials DockerCredentials `json:"docker_credentials,omitempty"` 80 Environment map[string]interface{} `json:"environment_json,omitempty"` 81} 82 83type App struct { 84 Guid string `json:"guid"` 85 CreatedAt string `json:"created_at"` 86 UpdatedAt string `json:"updated_at"` 87 Name string `json:"name"` 88 Memory int `json:"memory"` 89 Instances int `json:"instances"` 90 DiskQuota int `json:"disk_quota"` 91 SpaceGuid string `json:"space_guid"` 92 StackGuid string `json:"stack_guid"` 93 State string `json:"state"` 94 PackageState string `json:"package_state"` 95 Command string `json:"command"` 96 Buildpack string `json:"buildpack"` 97 DetectedBuildpack string `json:"detected_buildpack"` 98 DetectedBuildpackGuid string `json:"detected_buildpack_guid"` 99 HealthCheckHttpEndpoint string `json:"health_check_http_endpoint"` 100 HealthCheckType string `json:"health_check_type"` 101 HealthCheckTimeout int `json:"health_check_timeout"` 102 Diego bool `json:"diego"` 103 EnableSSH bool `json:"enable_ssh"` 104 DetectedStartCommand string `json:"detected_start_command"` 105 DockerImage string `json:"docker_image"` 106 DockerCredentials map[string]interface{} `json:"docker_credentials_json"` 107 Environment map[string]interface{} `json:"environment_json"` 108 StagingFailedReason string `json:"staging_failed_reason"` 109 StagingFailedDescription string `json:"staging_failed_description"` 110 Ports []int `json:"ports"` 111 SpaceURL string `json:"space_url"` 112 SpaceData SpaceResource `json:"space"` 113 PackageUpdatedAt string `json:"package_updated_at"` 114 c *Client 115} 116 117type AppInstance struct { 118 State string `json:"state"` 119 Since sinceTime `json:"since"` 120} 121 122type AppStats struct { 123 State string `json:"state"` 124 Stats struct { 125 Name string `json:"name"` 126 Uris []string `json:"uris"` 127 Host string `json:"host"` 128 Port int `json:"port"` 129 Uptime int `json:"uptime"` 130 MemQuota int `json:"mem_quota"` 131 DiskQuota int `json:"disk_quota"` 132 FdsQuota int `json:"fds_quota"` 133 Usage struct { 134 Time statTime `json:"time"` 135 CPU float64 `json:"cpu"` 136 Mem int `json:"mem"` 137 Disk int `json:"disk"` 138 } `json:"usage"` 139 } `json:"stats"` 140} 141 142type AppSummary struct { 143 Guid string `json:"guid"` 144 Name string `json:"name"` 145 ServiceCount int `json:"service_count"` 146 RunningInstances int `json:"running_instances"` 147 SpaceGuid string `json:"space_guid"` 148 StackGuid string `json:"stack_guid"` 149 Buildpack string `json:"buildpack"` 150 DetectedBuildpack string `json:"detected_buildpack"` 151 Environment map[string]interface{} `json:"environment_json"` 152 Memory int `json:"memory"` 153 Instances int `json:"instances"` 154 DiskQuota int `json:"disk_quota"` 155 State string `json:"state"` 156 Command string `json:"command"` 157 PackageState string `json:"package_state"` 158 HealthCheckType string `json:"health_check_type"` 159 HealthCheckTimeout int `json:"health_check_timeout"` 160 StagingFailedReason string `json:"staging_failed_reason"` 161 StagingFailedDescription string `json:"staging_failed_description"` 162 Diego bool `json:"diego"` 163 DockerImage string `json:"docker_image"` 164 DetectedStartCommand string `json:"detected_start_command"` 165 EnableSSH bool `json:"enable_ssh"` 166 DockerCredentials map[string]interface{} `json:"docker_credentials_json"` 167} 168 169type AppEnv struct { 170 // These can have arbitrary JSON so need to map to interface{} 171 Environment map[string]interface{} `json:"environment_json"` 172 StagingEnv map[string]interface{} `json:"staging_env_json"` 173 RunningEnv map[string]interface{} `json:"running_env_json"` 174 SystemEnv map[string]interface{} `json:"system_env_json"` 175 ApplicationEnv map[string]interface{} `json:"application_env_json"` 176} 177 178// Custom time types to handle non-RFC3339 formatting in API JSON 179 180type sinceTime struct { 181 time.Time 182} 183 184func (s *sinceTime) UnmarshalJSON(b []byte) (err error) { 185 timeFlt, err := strconv.ParseFloat(string(b), 64) 186 if err != nil { 187 return err 188 } 189 time := time.Unix(int64(timeFlt), 0) 190 *s = sinceTime{time} 191 return nil 192} 193 194func (s sinceTime) ToTime() time.Time { 195 t, _ := time.Parse(time.UnixDate, s.Format(time.UnixDate)) 196 return t 197} 198 199type statTime struct { 200 time.Time 201} 202 203func (s *statTime) UnmarshalJSON(b []byte) (err error) { 204 timeString, err := strconv.Unquote(string(b)) 205 if err != nil { 206 return err 207 } 208 209 possibleFormats := [...]string{time.RFC3339, time.RFC3339Nano, "2006-01-02 15:04:05 -0700", "2006-01-02 15:04:05 MST"} 210 211 var value time.Time 212 for _, possibleFormat := range possibleFormats { 213 if value, err = time.Parse(possibleFormat, timeString); err == nil { 214 *s = statTime{value} 215 return nil 216 } 217 } 218 219 return fmt.Errorf("%s was not in any of the expected Date Formats %v", timeString, possibleFormats) 220} 221 222func (s statTime) ToTime() time.Time { 223 t, _ := time.Parse(time.UnixDate, s.Format(time.UnixDate)) 224 return t 225} 226 227func (a *App) Space() (Space, error) { 228 var spaceResource SpaceResource 229 r := a.c.NewRequest("GET", a.SpaceURL) 230 resp, err := a.c.DoRequest(r) 231 if err != nil { 232 return Space{}, errors.Wrap(err, "Error requesting space") 233 } 234 defer resp.Body.Close() 235 resBody, err := ioutil.ReadAll(resp.Body) 236 if err != nil { 237 return Space{}, errors.Wrap(err, "Error reading space response") 238 } 239 240 err = json.Unmarshal(resBody, &spaceResource) 241 if err != nil { 242 return Space{}, errors.Wrap(err, "Error unmarshalling body") 243 } 244 return a.c.mergeSpaceResource(spaceResource), nil 245} 246 247func (a *App) Summary() (AppSummary, error) { 248 var appSummary AppSummary 249 requestUrl := fmt.Sprintf("/v2/apps/%s/summary", a.Guid) 250 r := a.c.NewRequest("GET", requestUrl) 251 resp, err := a.c.DoRequest(r) 252 if err != nil { 253 return AppSummary{}, errors.Wrap(err, "Error requesting app summary") 254 } 255 resBody, err := ioutil.ReadAll(resp.Body) 256 defer resp.Body.Close() 257 if err != nil { 258 return AppSummary{}, errors.Wrap(err, "Error reading app summary body") 259 } 260 err = json.Unmarshal(resBody, &appSummary) 261 if err != nil { 262 return AppSummary{}, errors.Wrap(err, "Error unmarshalling app summary") 263 } 264 return appSummary, nil 265} 266 267// ListAppsByQueryWithLimits queries totalPages app info. When totalPages is 268// less and equal than 0, it queries all app info 269// When there are no more than totalPages apps on server side, all apps info will be returned 270func (c *Client) ListAppsByQueryWithLimits(query url.Values, totalPages int) ([]App, error) { 271 return c.listApps("/v2/apps?"+query.Encode(), totalPages) 272} 273 274func (c *Client) ListAppsByQuery(query url.Values) ([]App, error) { 275 return c.listApps("/v2/apps?"+query.Encode(), -1) 276} 277 278// GetAppByGuidNoInlineCall will fetch app info including space and orgs information 279// Without using inline-relations-depth=2 call 280func (c *Client) GetAppByGuidNoInlineCall(guid string) (App, error) { 281 var appResource AppResource 282 r := c.NewRequest("GET", "/v2/apps/"+guid) 283 resp, err := c.DoRequest(r) 284 if err != nil { 285 return App{}, errors.Wrap(err, "Error requesting apps") 286 } 287 defer resp.Body.Close() 288 resBody, err := ioutil.ReadAll(resp.Body) 289 if err != nil { 290 return App{}, errors.Wrap(err, "Error reading app response body") 291 } 292 293 err = json.Unmarshal(resBody, &appResource) 294 if err != nil { 295 return App{}, errors.Wrap(err, "Error unmarshalling app") 296 } 297 app := c.mergeAppResource(appResource) 298 299 // If no Space Information no need to check org. 300 if app.SpaceGuid != "" { 301 //Getting Spaces Resource 302 space, err := app.Space() 303 if err != nil { 304 errors.Wrap(err, "Unable to get the Space for the apps "+app.Name) 305 } else { 306 app.SpaceData.Entity = space 307 308 } 309 310 //Getting orgResource 311 org, err := app.SpaceData.Entity.Org() 312 if err != nil { 313 errors.Wrap(err, "Unable to get the Org for the apps "+app.Name) 314 } else { 315 app.SpaceData.Entity.OrgData.Entity = org 316 } 317 } 318 319 return app, nil 320} 321 322func (c *Client) ListApps() ([]App, error) { 323 q := url.Values{} 324 q.Set("inline-relations-depth", "2") 325 return c.ListAppsByQuery(q) 326} 327 328func (c *Client) ListAppsByRoute(routeGuid string) ([]App, error) { 329 return c.listApps(fmt.Sprintf("/v2/routes/%s/apps", routeGuid), -1) 330} 331 332func (c *Client) listApps(requestUrl string, totalPages int) ([]App, error) { 333 pages := 0 334 apps := []App{} 335 for { 336 var appResp AppResponse 337 r := c.NewRequest("GET", requestUrl) 338 resp, err := c.DoRequest(r) 339 340 if err != nil { 341 return nil, errors.Wrap(err, "Error requesting apps") 342 } 343 defer resp.Body.Close() 344 resBody, err := ioutil.ReadAll(resp.Body) 345 if err != nil { 346 return nil, errors.Wrap(err, "Error reading app request") 347 } 348 349 err = json.Unmarshal(resBody, &appResp) 350 if err != nil { 351 return nil, errors.Wrap(err, "Error unmarshalling app") 352 } 353 for _, app := range appResp.Resources { 354 apps = append(apps, c.mergeAppResource(app)) 355 } 356 357 requestUrl = appResp.NextUrl 358 if requestUrl == "" { 359 break 360 } 361 362 pages += 1 363 if totalPages > 0 && pages >= totalPages { 364 break 365 } 366 } 367 return apps, nil 368} 369 370func (c *Client) GetAppInstances(guid string) (map[string]AppInstance, error) { 371 var appInstances map[string]AppInstance 372 373 requestURL := fmt.Sprintf("/v2/apps/%s/instances", guid) 374 r := c.NewRequest("GET", requestURL) 375 resp, err := c.DoRequest(r) 376 if err != nil { 377 return nil, errors.Wrap(err, "Error requesting app instances") 378 } 379 defer resp.Body.Close() 380 resBody, err := ioutil.ReadAll(resp.Body) 381 if err != nil { 382 return nil, errors.Wrap(err, "Error reading app instances") 383 } 384 err = json.Unmarshal(resBody, &appInstances) 385 if err != nil { 386 return nil, errors.Wrap(err, "Error unmarshalling app instances") 387 } 388 return appInstances, nil 389} 390 391func (c *Client) GetAppEnv(guid string) (AppEnv, error) { 392 var appEnv AppEnv 393 394 requestURL := fmt.Sprintf("/v2/apps/%s/env", guid) 395 r := c.NewRequest("GET", requestURL) 396 resp, err := c.DoRequest(r) 397 if err != nil { 398 return appEnv, errors.Wrap(err, "Error requesting app env") 399 } 400 defer resp.Body.Close() 401 resBody, err := ioutil.ReadAll(resp.Body) 402 if err != nil { 403 return appEnv, errors.Wrap(err, "Error reading app env") 404 } 405 err = json.Unmarshal(resBody, &appEnv) 406 if err != nil { 407 return appEnv, errors.Wrap(err, "Error unmarshalling app env") 408 } 409 return appEnv, nil 410} 411 412func (c *Client) GetAppRoutes(guid string) ([]Route, error) { 413 return c.fetchRoutes(fmt.Sprintf("/v2/apps/%s/routes", guid)) 414} 415 416func (c *Client) GetAppStats(guid string) (map[string]AppStats, error) { 417 var appStats map[string]AppStats 418 419 requestURL := fmt.Sprintf("/v2/apps/%s/stats", guid) 420 r := c.NewRequest("GET", requestURL) 421 resp, err := c.DoRequest(r) 422 if err != nil { 423 return nil, errors.Wrap(err, "Error requesting app stats") 424 } 425 defer resp.Body.Close() 426 resBody, err := ioutil.ReadAll(resp.Body) 427 if err != nil { 428 return nil, errors.Wrap(err, "Error reading app stats") 429 } 430 err = json.Unmarshal(resBody, &appStats) 431 if err != nil { 432 return nil, errors.Wrap(err, "Error unmarshalling app stats") 433 } 434 return appStats, nil 435} 436 437func (c *Client) KillAppInstance(guid string, index string) error { 438 requestURL := fmt.Sprintf("/v2/apps/%s/instances/%s", guid, index) 439 r := c.NewRequest("DELETE", requestURL) 440 resp, err := c.DoRequest(r) 441 if err != nil { 442 return errors.Wrapf(err, "Error stopping app %s at index %s", guid, index) 443 } 444 defer resp.Body.Close() 445 if resp.StatusCode != 204 { 446 return errors.Wrapf(err, "Error stopping app %s at index %s", guid, index) 447 } 448 return nil 449} 450 451func (c *Client) GetAppByGuid(guid string) (App, error) { 452 var appResource AppResource 453 r := c.NewRequest("GET", "/v2/apps/"+guid+"?inline-relations-depth=2") 454 resp, err := c.DoRequest(r) 455 if err != nil { 456 return App{}, errors.Wrap(err, "Error requesting apps") 457 } 458 defer resp.Body.Close() 459 resBody, err := ioutil.ReadAll(resp.Body) 460 if err != nil { 461 return App{}, errors.Wrap(err, "Error reading app response body") 462 } 463 464 err = json.Unmarshal(resBody, &appResource) 465 if err != nil { 466 return App{}, errors.Wrap(err, "Error unmarshalling app") 467 } 468 return c.mergeAppResource(appResource), nil 469} 470 471func (c *Client) AppByGuid(guid string) (App, error) { 472 return c.GetAppByGuid(guid) 473} 474 475//AppByName takes an appName, and GUIDs for a space and org, and performs 476// the API lookup with those query parameters set to return you the desired 477// App object. 478func (c *Client) AppByName(appName, spaceGuid, orgGuid string) (app App, err error) { 479 query := url.Values{} 480 query.Add("q", fmt.Sprintf("organization_guid:%s", orgGuid)) 481 query.Add("q", fmt.Sprintf("space_guid:%s", spaceGuid)) 482 query.Add("q", fmt.Sprintf("name:%s", appName)) 483 apps, err := c.ListAppsByQuery(query) 484 if err != nil { 485 return 486 } 487 if len(apps) == 0 { 488 err = fmt.Errorf("No app found with name: `%s` in space with GUID `%s` and org with GUID `%s`", appName, spaceGuid, orgGuid) 489 return 490 } 491 app = apps[0] 492 return 493} 494 495// UploadAppBits uploads the application's contents 496func (c *Client) UploadAppBits(file io.Reader, appGUID string) error { 497 requestFile, err := ioutil.TempFile("", "requests") 498 499 defer func() { 500 requestFile.Close() 501 os.Remove(requestFile.Name()) 502 }() 503 504 writer := multipart.NewWriter(requestFile) 505 err = writer.WriteField("resources", "[]") 506 if err != nil { 507 return errors.Wrapf(err, "Error uploading app %s bits", appGUID) 508 } 509 510 part, err := writer.CreateFormFile("application", "application.zip") 511 if err != nil { 512 return errors.Wrapf(err, "Error uploading app %s bits", appGUID) 513 } 514 515 _, err = io.Copy(part, file) 516 if err != nil { 517 return errors.Wrapf(err, "Error uploading app %s bits, failed to copy all bytes", appGUID) 518 } 519 520 err = writer.Close() 521 if err != nil { 522 return errors.Wrapf(err, "Error uploading app %s bits, failed to close multipart writer", appGUID) 523 } 524 525 requestFile.Seek(0, 0) 526 fileStats, err := requestFile.Stat() 527 if err != nil { 528 return errors.Wrapf(err, "Error uploading app %s bits, failed to get temp file stats", appGUID) 529 } 530 531 requestURL := fmt.Sprintf("/v2/apps/%s/bits", appGUID) 532 r := c.NewRequestWithBody("PUT", requestURL, requestFile) 533 req, err := r.toHTTP() 534 if err != nil { 535 return errors.Wrapf(err, "Error uploading app %s bits", appGUID) 536 } 537 538 req.ContentLength = fileStats.Size() 539 contentType := fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary()) 540 req.Header.Set("Content-Type", contentType) 541 542 resp, err := c.Do(req) 543 if err != nil { 544 return errors.Wrapf(err, "Error uploading app %s bits", appGUID) 545 } 546 if resp.StatusCode != http.StatusCreated { 547 return errors.Wrapf(err, "Error uploading app %s bits, response code: %d", appGUID, resp.StatusCode) 548 } 549 550 return nil 551} 552 553// GetAppBits downloads the application's bits as a tar file 554func (c *Client) GetAppBits(guid string) (io.ReadCloser, error) { 555 requestURL := fmt.Sprintf("/v2/apps/%s/download", guid) 556 req := c.NewRequest("GET", requestURL) 557 resp, err := c.DoRequestWithoutRedirects(req) 558 if err != nil { 559 return nil, errors.Wrapf(err, "Error downloading app %s bits, API request failed", guid) 560 } 561 if isResponseRedirect(resp) { 562 // directly download the bits from blobstore using a non cloud controller transport 563 // some blobstores will return a 400 if an Authorization header is sent 564 blobStoreLocation := resp.Header.Get("Location") 565 tr := &http.Transport{ 566 TLSClientConfig: &tls.Config{InsecureSkipVerify: c.Config.SkipSslValidation}, 567 } 568 client := &http.Client{Transport: tr} 569 resp, err = client.Get(blobStoreLocation) 570 if err != nil { 571 return nil, errors.Wrapf(err, "Error downloading app %s bits from blobstore", guid) 572 } 573 } else { 574 return nil, errors.Wrapf(err, "Error downloading app %s bits, expected redirect to blobstore", guid) 575 } 576 return resp.Body, nil 577} 578 579// CreateApp creates a new empty application that still needs it's 580// app bit uploaded and to be started 581func (c *Client) CreateApp(req AppCreateRequest) (App, error) { 582 var appResp AppResource 583 buf := bytes.NewBuffer(nil) 584 err := json.NewEncoder(buf).Encode(req) 585 if err != nil { 586 return App{}, err 587 } 588 r := c.NewRequestWithBody("POST", "/v2/apps", buf) 589 resp, err := c.DoRequest(r) 590 if err != nil { 591 return App{}, errors.Wrapf(err, "Error creating app %s", req.Name) 592 } 593 if resp.StatusCode != http.StatusCreated { 594 return App{}, errors.Wrapf(err, "Error creating app %s, response code: %d", req.Name, resp.StatusCode) 595 } 596 resBody, err := ioutil.ReadAll(resp.Body) 597 defer resp.Body.Close() 598 if err != nil { 599 return App{}, errors.Wrapf(err, "Error reading app %s http response body", req.Name) 600 } 601 err = json.Unmarshal(resBody, &appResp) 602 if err != nil { 603 return App{}, errors.Wrapf(err, "Error deserializing app %s response", req.Name) 604 } 605 return c.mergeAppResource(appResp), nil 606} 607 608func (c *Client) StartApp(guid string) error { 609 startRequest := strings.NewReader(`{ "state": "STARTED" }`) 610 resp, err := c.DoRequest(c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/apps/%s", guid), startRequest)) 611 if err != nil { 612 return err 613 } 614 if resp.StatusCode != http.StatusNoContent { 615 return errors.Wrapf(err, "Error starting app %s, response code: %d", guid, resp.StatusCode) 616 } 617 return nil 618} 619 620func (c *Client) StopApp(guid string) error { 621 stopRequest := strings.NewReader(`{ "state": "STOPPED" }`) 622 resp, err := c.DoRequest(c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/apps/%s", guid), stopRequest)) 623 if err != nil { 624 return err 625 } 626 if resp.StatusCode != http.StatusNoContent { 627 return errors.Wrapf(err, "Error stopping app %s, response code: %d", guid, resp.StatusCode) 628 } 629 return nil 630} 631 632func (c *Client) DeleteApp(guid string) error { 633 resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/apps/%s", guid))) 634 if err != nil { 635 return err 636 } 637 if resp.StatusCode != http.StatusNoContent { 638 return errors.Wrapf(err, "Error deleting app %s, response code: %d", guid, resp.StatusCode) 639 } 640 return nil 641} 642 643func (c *Client) mergeAppResource(app AppResource) App { 644 app.Entity.Guid = app.Meta.Guid 645 app.Entity.CreatedAt = app.Meta.CreatedAt 646 app.Entity.UpdatedAt = app.Meta.UpdatedAt 647 app.Entity.SpaceData.Entity.Guid = app.Entity.SpaceData.Meta.Guid 648 app.Entity.SpaceData.Entity.OrgData.Entity.Guid = app.Entity.SpaceData.Entity.OrgData.Meta.Guid 649 app.Entity.c = c 650 return app.Entity 651} 652 653func isResponseRedirect(res *http.Response) bool { 654 switch res.StatusCode { 655 case http.StatusTemporaryRedirect, http.StatusPermanentRedirect, http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther: 656 return true 657 } 658 return false 659} 660