660 lines
21 KiB
Go
660 lines
21 KiB
Go
|
package cfclient
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"crypto/tls"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"mime/multipart"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"os"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/pkg/errors"
|
||
|
)
|
||
|
|
||
|
type AppResponse struct {
|
||
|
Count int `json:"total_results"`
|
||
|
Pages int `json:"total_pages"`
|
||
|
NextUrl string `json:"next_url"`
|
||
|
Resources []AppResource `json:"resources"`
|
||
|
}
|
||
|
|
||
|
type AppResource struct {
|
||
|
Meta Meta `json:"metadata"`
|
||
|
Entity App `json:"entity"`
|
||
|
}
|
||
|
|
||
|
type AppState string
|
||
|
|
||
|
const (
|
||
|
APP_STOPPED AppState = "STOPPED"
|
||
|
APP_STARTED AppState = "STARTED"
|
||
|
)
|
||
|
|
||
|
type HealthCheckType string
|
||
|
|
||
|
const (
|
||
|
HEALTH_HTTP HealthCheckType = "http"
|
||
|
HEALTH_PORT HealthCheckType = "port"
|
||
|
HEALTH_PROCESS HealthCheckType = "process"
|
||
|
)
|
||
|
|
||
|
type DockerCredentials struct {
|
||
|
Username string `json:"username,omitempty"`
|
||
|
Password string `json:"password,omitempty"`
|
||
|
}
|
||
|
|
||
|
type AppCreateRequest struct {
|
||
|
Name string `json:"name"`
|
||
|
SpaceGuid string `json:"space_guid"`
|
||
|
// Memory for the app, in MB
|
||
|
Memory int `json:"memory,omitempty"`
|
||
|
// Instances to startup
|
||
|
Instances int `json:"instances,omitempty"`
|
||
|
// Disk quota in MB
|
||
|
DiskQuota int `json:"disk_quota,omitempty"`
|
||
|
StackGuid string `json:"stack_guid,omitempty"`
|
||
|
// Desired state of the app. Either "STOPPED" or "STARTED"
|
||
|
State AppState `json:"state,omitempty"`
|
||
|
// Command to start an app
|
||
|
Command string `json:"command,omitempty"`
|
||
|
// Buildpack to build the app. Three options:
|
||
|
// 1. Blank for autodetection
|
||
|
// 2. GIT url
|
||
|
// 3. Name of an installed buildpack
|
||
|
Buildpack string `json:"buildpack,omitempty"`
|
||
|
// Endpoint to check if an app is healthy
|
||
|
HealthCheckHttpEndpoint string `json:"health_check_http_endpoint,omitempty"`
|
||
|
// How to check if an app is healthy. Defaults to HEALTH_PORT if not specified
|
||
|
HealthCheckType HealthCheckType `json:"health_check_type,omitempty"`
|
||
|
Diego bool `json:"diego,omitempty"`
|
||
|
EnableSSH bool `json:"enable_ssh,omitempty"`
|
||
|
DockerImage string `json:"docker_image,omitempty"`
|
||
|
DockerCredentials DockerCredentials `json:"docker_credentials,omitempty"`
|
||
|
Environment map[string]interface{} `json:"environment_json,omitempty"`
|
||
|
}
|
||
|
|
||
|
type App struct {
|
||
|
Guid string `json:"guid"`
|
||
|
CreatedAt string `json:"created_at"`
|
||
|
UpdatedAt string `json:"updated_at"`
|
||
|
Name string `json:"name"`
|
||
|
Memory int `json:"memory"`
|
||
|
Instances int `json:"instances"`
|
||
|
DiskQuota int `json:"disk_quota"`
|
||
|
SpaceGuid string `json:"space_guid"`
|
||
|
StackGuid string `json:"stack_guid"`
|
||
|
State string `json:"state"`
|
||
|
PackageState string `json:"package_state"`
|
||
|
Command string `json:"command"`
|
||
|
Buildpack string `json:"buildpack"`
|
||
|
DetectedBuildpack string `json:"detected_buildpack"`
|
||
|
DetectedBuildpackGuid string `json:"detected_buildpack_guid"`
|
||
|
HealthCheckHttpEndpoint string `json:"health_check_http_endpoint"`
|
||
|
HealthCheckType string `json:"health_check_type"`
|
||
|
HealthCheckTimeout int `json:"health_check_timeout"`
|
||
|
Diego bool `json:"diego"`
|
||
|
EnableSSH bool `json:"enable_ssh"`
|
||
|
DetectedStartCommand string `json:"detected_start_command"`
|
||
|
DockerImage string `json:"docker_image"`
|
||
|
DockerCredentials map[string]interface{} `json:"docker_credentials_json"`
|
||
|
Environment map[string]interface{} `json:"environment_json"`
|
||
|
StagingFailedReason string `json:"staging_failed_reason"`
|
||
|
StagingFailedDescription string `json:"staging_failed_description"`
|
||
|
Ports []int `json:"ports"`
|
||
|
SpaceURL string `json:"space_url"`
|
||
|
SpaceData SpaceResource `json:"space"`
|
||
|
PackageUpdatedAt string `json:"package_updated_at"`
|
||
|
c *Client
|
||
|
}
|
||
|
|
||
|
type AppInstance struct {
|
||
|
State string `json:"state"`
|
||
|
Since sinceTime `json:"since"`
|
||
|
}
|
||
|
|
||
|
type AppStats struct {
|
||
|
State string `json:"state"`
|
||
|
Stats struct {
|
||
|
Name string `json:"name"`
|
||
|
Uris []string `json:"uris"`
|
||
|
Host string `json:"host"`
|
||
|
Port int `json:"port"`
|
||
|
Uptime int `json:"uptime"`
|
||
|
MemQuota int `json:"mem_quota"`
|
||
|
DiskQuota int `json:"disk_quota"`
|
||
|
FdsQuota int `json:"fds_quota"`
|
||
|
Usage struct {
|
||
|
Time statTime `json:"time"`
|
||
|
CPU float64 `json:"cpu"`
|
||
|
Mem int `json:"mem"`
|
||
|
Disk int `json:"disk"`
|
||
|
} `json:"usage"`
|
||
|
} `json:"stats"`
|
||
|
}
|
||
|
|
||
|
type AppSummary struct {
|
||
|
Guid string `json:"guid"`
|
||
|
Name string `json:"name"`
|
||
|
ServiceCount int `json:"service_count"`
|
||
|
RunningInstances int `json:"running_instances"`
|
||
|
SpaceGuid string `json:"space_guid"`
|
||
|
StackGuid string `json:"stack_guid"`
|
||
|
Buildpack string `json:"buildpack"`
|
||
|
DetectedBuildpack string `json:"detected_buildpack"`
|
||
|
Environment map[string]interface{} `json:"environment_json"`
|
||
|
Memory int `json:"memory"`
|
||
|
Instances int `json:"instances"`
|
||
|
DiskQuota int `json:"disk_quota"`
|
||
|
State string `json:"state"`
|
||
|
Command string `json:"command"`
|
||
|
PackageState string `json:"package_state"`
|
||
|
HealthCheckType string `json:"health_check_type"`
|
||
|
HealthCheckTimeout int `json:"health_check_timeout"`
|
||
|
StagingFailedReason string `json:"staging_failed_reason"`
|
||
|
StagingFailedDescription string `json:"staging_failed_description"`
|
||
|
Diego bool `json:"diego"`
|
||
|
DockerImage string `json:"docker_image"`
|
||
|
DetectedStartCommand string `json:"detected_start_command"`
|
||
|
EnableSSH bool `json:"enable_ssh"`
|
||
|
DockerCredentials map[string]interface{} `json:"docker_credentials_json"`
|
||
|
}
|
||
|
|
||
|
type AppEnv struct {
|
||
|
// These can have arbitrary JSON so need to map to interface{}
|
||
|
Environment map[string]interface{} `json:"environment_json"`
|
||
|
StagingEnv map[string]interface{} `json:"staging_env_json"`
|
||
|
RunningEnv map[string]interface{} `json:"running_env_json"`
|
||
|
SystemEnv map[string]interface{} `json:"system_env_json"`
|
||
|
ApplicationEnv map[string]interface{} `json:"application_env_json"`
|
||
|
}
|
||
|
|
||
|
// Custom time types to handle non-RFC3339 formatting in API JSON
|
||
|
|
||
|
type sinceTime struct {
|
||
|
time.Time
|
||
|
}
|
||
|
|
||
|
func (s *sinceTime) UnmarshalJSON(b []byte) (err error) {
|
||
|
timeFlt, err := strconv.ParseFloat(string(b), 64)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
time := time.Unix(int64(timeFlt), 0)
|
||
|
*s = sinceTime{time}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s sinceTime) ToTime() time.Time {
|
||
|
t, _ := time.Parse(time.UnixDate, s.Format(time.UnixDate))
|
||
|
return t
|
||
|
}
|
||
|
|
||
|
type statTime struct {
|
||
|
time.Time
|
||
|
}
|
||
|
|
||
|
func (s *statTime) UnmarshalJSON(b []byte) (err error) {
|
||
|
timeString, err := strconv.Unquote(string(b))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
possibleFormats := [...]string{time.RFC3339, time.RFC3339Nano, "2006-01-02 15:04:05 -0700", "2006-01-02 15:04:05 MST"}
|
||
|
|
||
|
var value time.Time
|
||
|
for _, possibleFormat := range possibleFormats {
|
||
|
if value, err = time.Parse(possibleFormat, timeString); err == nil {
|
||
|
*s = statTime{value}
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return fmt.Errorf("%s was not in any of the expected Date Formats %v", timeString, possibleFormats)
|
||
|
}
|
||
|
|
||
|
func (s statTime) ToTime() time.Time {
|
||
|
t, _ := time.Parse(time.UnixDate, s.Format(time.UnixDate))
|
||
|
return t
|
||
|
}
|
||
|
|
||
|
func (a *App) Space() (Space, error) {
|
||
|
var spaceResource SpaceResource
|
||
|
r := a.c.NewRequest("GET", a.SpaceURL)
|
||
|
resp, err := a.c.DoRequest(r)
|
||
|
if err != nil {
|
||
|
return Space{}, errors.Wrap(err, "Error requesting space")
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
resBody, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return Space{}, errors.Wrap(err, "Error reading space response")
|
||
|
}
|
||
|
|
||
|
err = json.Unmarshal(resBody, &spaceResource)
|
||
|
if err != nil {
|
||
|
return Space{}, errors.Wrap(err, "Error unmarshalling body")
|
||
|
}
|
||
|
return a.c.mergeSpaceResource(spaceResource), nil
|
||
|
}
|
||
|
|
||
|
func (a *App) Summary() (AppSummary, error) {
|
||
|
var appSummary AppSummary
|
||
|
requestUrl := fmt.Sprintf("/v2/apps/%s/summary", a.Guid)
|
||
|
r := a.c.NewRequest("GET", requestUrl)
|
||
|
resp, err := a.c.DoRequest(r)
|
||
|
if err != nil {
|
||
|
return AppSummary{}, errors.Wrap(err, "Error requesting app summary")
|
||
|
}
|
||
|
resBody, err := ioutil.ReadAll(resp.Body)
|
||
|
defer resp.Body.Close()
|
||
|
if err != nil {
|
||
|
return AppSummary{}, errors.Wrap(err, "Error reading app summary body")
|
||
|
}
|
||
|
err = json.Unmarshal(resBody, &appSummary)
|
||
|
if err != nil {
|
||
|
return AppSummary{}, errors.Wrap(err, "Error unmarshalling app summary")
|
||
|
}
|
||
|
return appSummary, nil
|
||
|
}
|
||
|
|
||
|
// ListAppsByQueryWithLimits queries totalPages app info. When totalPages is
|
||
|
// less and equal than 0, it queries all app info
|
||
|
// When there are no more than totalPages apps on server side, all apps info will be returned
|
||
|
func (c *Client) ListAppsByQueryWithLimits(query url.Values, totalPages int) ([]App, error) {
|
||
|
return c.listApps("/v2/apps?"+query.Encode(), totalPages)
|
||
|
}
|
||
|
|
||
|
func (c *Client) ListAppsByQuery(query url.Values) ([]App, error) {
|
||
|
return c.listApps("/v2/apps?"+query.Encode(), -1)
|
||
|
}
|
||
|
|
||
|
// GetAppByGuidNoInlineCall will fetch app info including space and orgs information
|
||
|
// Without using inline-relations-depth=2 call
|
||
|
func (c *Client) GetAppByGuidNoInlineCall(guid string) (App, error) {
|
||
|
var appResource AppResource
|
||
|
r := c.NewRequest("GET", "/v2/apps/"+guid)
|
||
|
resp, err := c.DoRequest(r)
|
||
|
if err != nil {
|
||
|
return App{}, errors.Wrap(err, "Error requesting apps")
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
resBody, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return App{}, errors.Wrap(err, "Error reading app response body")
|
||
|
}
|
||
|
|
||
|
err = json.Unmarshal(resBody, &appResource)
|
||
|
if err != nil {
|
||
|
return App{}, errors.Wrap(err, "Error unmarshalling app")
|
||
|
}
|
||
|
app := c.mergeAppResource(appResource)
|
||
|
|
||
|
// If no Space Information no need to check org.
|
||
|
if app.SpaceGuid != "" {
|
||
|
//Getting Spaces Resource
|
||
|
space, err := app.Space()
|
||
|
if err != nil {
|
||
|
errors.Wrap(err, "Unable to get the Space for the apps "+app.Name)
|
||
|
} else {
|
||
|
app.SpaceData.Entity = space
|
||
|
|
||
|
}
|
||
|
|
||
|
//Getting orgResource
|
||
|
org, err := app.SpaceData.Entity.Org()
|
||
|
if err != nil {
|
||
|
errors.Wrap(err, "Unable to get the Org for the apps "+app.Name)
|
||
|
} else {
|
||
|
app.SpaceData.Entity.OrgData.Entity = org
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return app, nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) ListApps() ([]App, error) {
|
||
|
q := url.Values{}
|
||
|
q.Set("inline-relations-depth", "2")
|
||
|
return c.ListAppsByQuery(q)
|
||
|
}
|
||
|
|
||
|
func (c *Client) ListAppsByRoute(routeGuid string) ([]App, error) {
|
||
|
return c.listApps(fmt.Sprintf("/v2/routes/%s/apps", routeGuid), -1)
|
||
|
}
|
||
|
|
||
|
func (c *Client) listApps(requestUrl string, totalPages int) ([]App, error) {
|
||
|
pages := 0
|
||
|
apps := []App{}
|
||
|
for {
|
||
|
var appResp AppResponse
|
||
|
r := c.NewRequest("GET", requestUrl)
|
||
|
resp, err := c.DoRequest(r)
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "Error requesting apps")
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
resBody, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "Error reading app request")
|
||
|
}
|
||
|
|
||
|
err = json.Unmarshal(resBody, &appResp)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "Error unmarshalling app")
|
||
|
}
|
||
|
for _, app := range appResp.Resources {
|
||
|
apps = append(apps, c.mergeAppResource(app))
|
||
|
}
|
||
|
|
||
|
requestUrl = appResp.NextUrl
|
||
|
if requestUrl == "" {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
pages += 1
|
||
|
if totalPages > 0 && pages >= totalPages {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
return apps, nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) GetAppInstances(guid string) (map[string]AppInstance, error) {
|
||
|
var appInstances map[string]AppInstance
|
||
|
|
||
|
requestURL := fmt.Sprintf("/v2/apps/%s/instances", guid)
|
||
|
r := c.NewRequest("GET", requestURL)
|
||
|
resp, err := c.DoRequest(r)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "Error requesting app instances")
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
resBody, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "Error reading app instances")
|
||
|
}
|
||
|
err = json.Unmarshal(resBody, &appInstances)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "Error unmarshalling app instances")
|
||
|
}
|
||
|
return appInstances, nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) GetAppEnv(guid string) (AppEnv, error) {
|
||
|
var appEnv AppEnv
|
||
|
|
||
|
requestURL := fmt.Sprintf("/v2/apps/%s/env", guid)
|
||
|
r := c.NewRequest("GET", requestURL)
|
||
|
resp, err := c.DoRequest(r)
|
||
|
if err != nil {
|
||
|
return appEnv, errors.Wrap(err, "Error requesting app env")
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
resBody, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return appEnv, errors.Wrap(err, "Error reading app env")
|
||
|
}
|
||
|
err = json.Unmarshal(resBody, &appEnv)
|
||
|
if err != nil {
|
||
|
return appEnv, errors.Wrap(err, "Error unmarshalling app env")
|
||
|
}
|
||
|
return appEnv, nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) GetAppRoutes(guid string) ([]Route, error) {
|
||
|
return c.fetchRoutes(fmt.Sprintf("/v2/apps/%s/routes", guid))
|
||
|
}
|
||
|
|
||
|
func (c *Client) GetAppStats(guid string) (map[string]AppStats, error) {
|
||
|
var appStats map[string]AppStats
|
||
|
|
||
|
requestURL := fmt.Sprintf("/v2/apps/%s/stats", guid)
|
||
|
r := c.NewRequest("GET", requestURL)
|
||
|
resp, err := c.DoRequest(r)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "Error requesting app stats")
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
resBody, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "Error reading app stats")
|
||
|
}
|
||
|
err = json.Unmarshal(resBody, &appStats)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "Error unmarshalling app stats")
|
||
|
}
|
||
|
return appStats, nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) KillAppInstance(guid string, index string) error {
|
||
|
requestURL := fmt.Sprintf("/v2/apps/%s/instances/%s", guid, index)
|
||
|
r := c.NewRequest("DELETE", requestURL)
|
||
|
resp, err := c.DoRequest(r)
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "Error stopping app %s at index %s", guid, index)
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
if resp.StatusCode != 204 {
|
||
|
return errors.Wrapf(err, "Error stopping app %s at index %s", guid, index)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) GetAppByGuid(guid string) (App, error) {
|
||
|
var appResource AppResource
|
||
|
r := c.NewRequest("GET", "/v2/apps/"+guid+"?inline-relations-depth=2")
|
||
|
resp, err := c.DoRequest(r)
|
||
|
if err != nil {
|
||
|
return App{}, errors.Wrap(err, "Error requesting apps")
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
resBody, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return App{}, errors.Wrap(err, "Error reading app response body")
|
||
|
}
|
||
|
|
||
|
err = json.Unmarshal(resBody, &appResource)
|
||
|
if err != nil {
|
||
|
return App{}, errors.Wrap(err, "Error unmarshalling app")
|
||
|
}
|
||
|
return c.mergeAppResource(appResource), nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) AppByGuid(guid string) (App, error) {
|
||
|
return c.GetAppByGuid(guid)
|
||
|
}
|
||
|
|
||
|
//AppByName takes an appName, and GUIDs for a space and org, and performs
|
||
|
// the API lookup with those query parameters set to return you the desired
|
||
|
// App object.
|
||
|
func (c *Client) AppByName(appName, spaceGuid, orgGuid string) (app App, err error) {
|
||
|
query := url.Values{}
|
||
|
query.Add("q", fmt.Sprintf("organization_guid:%s", orgGuid))
|
||
|
query.Add("q", fmt.Sprintf("space_guid:%s", spaceGuid))
|
||
|
query.Add("q", fmt.Sprintf("name:%s", appName))
|
||
|
apps, err := c.ListAppsByQuery(query)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
if len(apps) == 0 {
|
||
|
err = fmt.Errorf("No app found with name: `%s` in space with GUID `%s` and org with GUID `%s`", appName, spaceGuid, orgGuid)
|
||
|
return
|
||
|
}
|
||
|
app = apps[0]
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// UploadAppBits uploads the application's contents
|
||
|
func (c *Client) UploadAppBits(file io.Reader, appGUID string) error {
|
||
|
requestFile, err := ioutil.TempFile("", "requests")
|
||
|
|
||
|
defer func() {
|
||
|
requestFile.Close()
|
||
|
os.Remove(requestFile.Name())
|
||
|
}()
|
||
|
|
||
|
writer := multipart.NewWriter(requestFile)
|
||
|
err = writer.WriteField("resources", "[]")
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "Error uploading app %s bits", appGUID)
|
||
|
}
|
||
|
|
||
|
part, err := writer.CreateFormFile("application", "application.zip")
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "Error uploading app %s bits", appGUID)
|
||
|
}
|
||
|
|
||
|
_, err = io.Copy(part, file)
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "Error uploading app %s bits, failed to copy all bytes", appGUID)
|
||
|
}
|
||
|
|
||
|
err = writer.Close()
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "Error uploading app %s bits, failed to close multipart writer", appGUID)
|
||
|
}
|
||
|
|
||
|
requestFile.Seek(0, 0)
|
||
|
fileStats, err := requestFile.Stat()
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "Error uploading app %s bits, failed to get temp file stats", appGUID)
|
||
|
}
|
||
|
|
||
|
requestURL := fmt.Sprintf("/v2/apps/%s/bits", appGUID)
|
||
|
r := c.NewRequestWithBody("PUT", requestURL, requestFile)
|
||
|
req, err := r.toHTTP()
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "Error uploading app %s bits", appGUID)
|
||
|
}
|
||
|
|
||
|
req.ContentLength = fileStats.Size()
|
||
|
contentType := fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary())
|
||
|
req.Header.Set("Content-Type", contentType)
|
||
|
|
||
|
resp, err := c.Do(req)
|
||
|
if err != nil {
|
||
|
return errors.Wrapf(err, "Error uploading app %s bits", appGUID)
|
||
|
}
|
||
|
if resp.StatusCode != http.StatusCreated {
|
||
|
return errors.Wrapf(err, "Error uploading app %s bits, response code: %d", appGUID, resp.StatusCode)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// GetAppBits downloads the application's bits as a tar file
|
||
|
func (c *Client) GetAppBits(guid string) (io.ReadCloser, error) {
|
||
|
requestURL := fmt.Sprintf("/v2/apps/%s/download", guid)
|
||
|
req := c.NewRequest("GET", requestURL)
|
||
|
resp, err := c.DoRequestWithoutRedirects(req)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrapf(err, "Error downloading app %s bits, API request failed", guid)
|
||
|
}
|
||
|
if isResponseRedirect(resp) {
|
||
|
// directly download the bits from blobstore using a non cloud controller transport
|
||
|
// some blobstores will return a 400 if an Authorization header is sent
|
||
|
blobStoreLocation := resp.Header.Get("Location")
|
||
|
tr := &http.Transport{
|
||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.Config.SkipSslValidation},
|
||
|
}
|
||
|
client := &http.Client{Transport: tr}
|
||
|
resp, err = client.Get(blobStoreLocation)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrapf(err, "Error downloading app %s bits from blobstore", guid)
|
||
|
}
|
||
|
} else {
|
||
|
return nil, errors.Wrapf(err, "Error downloading app %s bits, expected redirect to blobstore", guid)
|
||
|
}
|
||
|
return resp.Body, nil
|
||
|
}
|
||
|
|
||
|
// CreateApp creates a new empty application that still needs it's
|
||
|
// app bit uploaded and to be started
|
||
|
func (c *Client) CreateApp(req AppCreateRequest) (App, error) {
|
||
|
var appResp AppResource
|
||
|
buf := bytes.NewBuffer(nil)
|
||
|
err := json.NewEncoder(buf).Encode(req)
|
||
|
if err != nil {
|
||
|
return App{}, err
|
||
|
}
|
||
|
r := c.NewRequestWithBody("POST", "/v2/apps", buf)
|
||
|
resp, err := c.DoRequest(r)
|
||
|
if err != nil {
|
||
|
return App{}, errors.Wrapf(err, "Error creating app %s", req.Name)
|
||
|
}
|
||
|
if resp.StatusCode != http.StatusCreated {
|
||
|
return App{}, errors.Wrapf(err, "Error creating app %s, response code: %d", req.Name, resp.StatusCode)
|
||
|
}
|
||
|
resBody, err := ioutil.ReadAll(resp.Body)
|
||
|
defer resp.Body.Close()
|
||
|
if err != nil {
|
||
|
return App{}, errors.Wrapf(err, "Error reading app %s http response body", req.Name)
|
||
|
}
|
||
|
err = json.Unmarshal(resBody, &appResp)
|
||
|
if err != nil {
|
||
|
return App{}, errors.Wrapf(err, "Error deserializing app %s response", req.Name)
|
||
|
}
|
||
|
return c.mergeAppResource(appResp), nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) StartApp(guid string) error {
|
||
|
startRequest := strings.NewReader(`{ "state": "STARTED" }`)
|
||
|
resp, err := c.DoRequest(c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/apps/%s", guid), startRequest))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if resp.StatusCode != http.StatusNoContent {
|
||
|
return errors.Wrapf(err, "Error starting app %s, response code: %d", guid, resp.StatusCode)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) StopApp(guid string) error {
|
||
|
stopRequest := strings.NewReader(`{ "state": "STOPPED" }`)
|
||
|
resp, err := c.DoRequest(c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/apps/%s", guid), stopRequest))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if resp.StatusCode != http.StatusNoContent {
|
||
|
return errors.Wrapf(err, "Error stopping app %s, response code: %d", guid, resp.StatusCode)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) DeleteApp(guid string) error {
|
||
|
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/apps/%s", guid)))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if resp.StatusCode != http.StatusNoContent {
|
||
|
return errors.Wrapf(err, "Error deleting app %s, response code: %d", guid, resp.StatusCode)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) mergeAppResource(app AppResource) App {
|
||
|
app.Entity.Guid = app.Meta.Guid
|
||
|
app.Entity.CreatedAt = app.Meta.CreatedAt
|
||
|
app.Entity.UpdatedAt = app.Meta.UpdatedAt
|
||
|
app.Entity.SpaceData.Entity.Guid = app.Entity.SpaceData.Meta.Guid
|
||
|
app.Entity.SpaceData.Entity.OrgData.Entity.Guid = app.Entity.SpaceData.Entity.OrgData.Meta.Guid
|
||
|
app.Entity.c = c
|
||
|
return app.Entity
|
||
|
}
|
||
|
|
||
|
func isResponseRedirect(res *http.Response) bool {
|
||
|
switch res.StatusCode {
|
||
|
case http.StatusTemporaryRedirect, http.StatusPermanentRedirect, http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther:
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
}
|