open-vault/vendor/github.com/cloudfoundry-community/go-cfclient/apps.go
2019-06-06 12:26:04 -07:00

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
}