2017-02-07 21:33:23 +00:00
|
|
|
/*
|
2017-12-19 15:00:09 +00:00
|
|
|
Copyright 2014 The go-marathon Authors All rights reserved.
|
2017-02-07 21:33:23 +00:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package marathon
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
2017-12-19 15:00:09 +00:00
|
|
|
"net"
|
2017-02-07 21:33:23 +00:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"regexp"
|
2017-05-19 12:24:28 +00:00
|
|
|
"strings"
|
2017-02-07 21:33:23 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Marathon is the interface to the marathon API
|
|
|
|
type Marathon interface {
|
|
|
|
// -- APPLICATIONS ---
|
|
|
|
|
|
|
|
// get a listing of the application ids
|
|
|
|
ListApplications(url.Values) ([]string, error)
|
|
|
|
// a list of application versions
|
|
|
|
ApplicationVersions(name string) (*ApplicationVersions, error)
|
|
|
|
// check a application version exists
|
|
|
|
HasApplicationVersion(name, version string) (bool, error)
|
|
|
|
// change an application to a different version
|
|
|
|
SetApplicationVersion(name string, version *ApplicationVersion) (*DeploymentID, error)
|
|
|
|
// check if an application is ok
|
|
|
|
ApplicationOK(name string) (bool, error)
|
|
|
|
// create an application in marathon
|
|
|
|
CreateApplication(application *Application) (*Application, error)
|
|
|
|
// delete an application
|
|
|
|
DeleteApplication(name string, force bool) (*DeploymentID, error)
|
|
|
|
// update an application in marathon
|
|
|
|
UpdateApplication(application *Application, force bool) (*DeploymentID, error)
|
|
|
|
// a list of deployments on a application
|
|
|
|
ApplicationDeployments(name string) ([]*DeploymentID, error)
|
|
|
|
// scale a application
|
|
|
|
ScaleApplicationInstances(name string, instances int, force bool) (*DeploymentID, error)
|
|
|
|
// restart an application
|
|
|
|
RestartApplication(name string, force bool) (*DeploymentID, error)
|
|
|
|
// get a list of applications from marathon
|
|
|
|
Applications(url.Values) (*Applications, error)
|
|
|
|
// get an application by name
|
|
|
|
Application(name string) (*Application, error)
|
|
|
|
// get an application by options
|
|
|
|
ApplicationBy(name string, opts *GetAppOpts) (*Application, error)
|
|
|
|
// get an application by name and version
|
|
|
|
ApplicationByVersion(name, version string) (*Application, error)
|
|
|
|
// wait of application
|
|
|
|
WaitOnApplication(name string, timeout time.Duration) error
|
|
|
|
|
2018-07-03 21:42:03 +00:00
|
|
|
// -- PODS ---
|
|
|
|
// whether this version of Marathon supports pods
|
|
|
|
SupportsPods() (bool, error)
|
|
|
|
|
|
|
|
// get pod status
|
|
|
|
PodStatus(name string) (*PodStatus, error)
|
|
|
|
// get all pod statuses
|
|
|
|
PodStatuses() ([]*PodStatus, error)
|
|
|
|
|
|
|
|
// get pod
|
|
|
|
Pod(name string) (*Pod, error)
|
|
|
|
// get all pods
|
|
|
|
Pods() ([]Pod, error)
|
|
|
|
// create pod
|
|
|
|
CreatePod(pod *Pod) (*Pod, error)
|
|
|
|
// update pod
|
|
|
|
UpdatePod(pod *Pod, force bool) (*Pod, error)
|
|
|
|
// delete pod
|
|
|
|
DeletePod(name string, force bool) (*DeploymentID, error)
|
|
|
|
// wait on pod to be deployed
|
|
|
|
WaitOnPod(name string, timeout time.Duration) error
|
|
|
|
// check if a pod is running
|
|
|
|
PodIsRunning(name string) bool
|
|
|
|
|
|
|
|
// get versions of a pod
|
|
|
|
PodVersions(name string) ([]string, error)
|
|
|
|
// get pod by version
|
|
|
|
PodByVersion(name, version string) (*Pod, error)
|
|
|
|
|
|
|
|
// delete instances of a pod
|
|
|
|
DeletePodInstances(name string, instances []string) ([]*PodInstance, error)
|
|
|
|
// delete pod instance
|
|
|
|
DeletePodInstance(name, instance string) (*PodInstance, error)
|
|
|
|
|
2017-02-07 21:33:23 +00:00
|
|
|
// -- TASKS ---
|
|
|
|
|
|
|
|
// get a list of tasks for a specific application
|
|
|
|
Tasks(application string) (*Tasks, error)
|
|
|
|
// get a list of all tasks
|
|
|
|
AllTasks(opts *AllTasksOpts) (*Tasks, error)
|
|
|
|
// get the endpoints for a service on a application
|
|
|
|
TaskEndpoints(name string, port int, healthCheck bool) ([]string, error)
|
|
|
|
// kill all the tasks for any application
|
|
|
|
KillApplicationTasks(applicationID string, opts *KillApplicationTasksOpts) (*Tasks, error)
|
|
|
|
// kill a single task
|
|
|
|
KillTask(taskID string, opts *KillTaskOpts) (*Task, error)
|
|
|
|
// kill the given array of tasks
|
|
|
|
KillTasks(taskIDs []string, opts *KillTaskOpts) error
|
|
|
|
|
|
|
|
// --- GROUPS ---
|
|
|
|
|
|
|
|
// list all the groups in the system
|
|
|
|
Groups() (*Groups, error)
|
|
|
|
// retrieve a specific group from marathon
|
|
|
|
Group(name string) (*Group, error)
|
|
|
|
// list all groups in marathon by options
|
|
|
|
GroupsBy(opts *GetGroupOpts) (*Groups, error)
|
|
|
|
// retrieve a specific group from marathon by options
|
|
|
|
GroupBy(name string, opts *GetGroupOpts) (*Group, error)
|
|
|
|
// create a group deployment
|
|
|
|
CreateGroup(group *Group) error
|
|
|
|
// delete a group
|
|
|
|
DeleteGroup(name string, force bool) (*DeploymentID, error)
|
|
|
|
// update a groups
|
|
|
|
UpdateGroup(id string, group *Group, force bool) (*DeploymentID, error)
|
|
|
|
// check if a group exists
|
|
|
|
HasGroup(name string) (bool, error)
|
|
|
|
// wait for an group to be deployed
|
|
|
|
WaitOnGroup(name string, timeout time.Duration) error
|
|
|
|
|
|
|
|
// --- DEPLOYMENTS ---
|
|
|
|
|
|
|
|
// get a list of the deployments
|
|
|
|
Deployments() ([]*Deployment, error)
|
|
|
|
// delete a deployment
|
|
|
|
DeleteDeployment(id string, force bool) (*DeploymentID, error)
|
|
|
|
// check to see if a deployment exists
|
|
|
|
HasDeployment(id string) (bool, error)
|
|
|
|
// wait of a deployment to finish
|
|
|
|
WaitOnDeployment(id string, timeout time.Duration) error
|
|
|
|
|
|
|
|
// --- SUBSCRIPTIONS ---
|
|
|
|
|
|
|
|
// a list of current subscriptions
|
|
|
|
Subscriptions() (*Subscriptions, error)
|
|
|
|
// add a events listener
|
|
|
|
AddEventsListener(filter int) (EventsChannel, error)
|
|
|
|
// remove a events listener
|
|
|
|
RemoveEventsListener(channel EventsChannel)
|
|
|
|
// Subscribe a callback URL
|
|
|
|
Subscribe(string) error
|
|
|
|
// Unsubscribe a callback URL
|
|
|
|
Unsubscribe(string) error
|
|
|
|
|
|
|
|
// --- QUEUE ---
|
|
|
|
// get marathon launch queue
|
|
|
|
Queue() (*Queue, error)
|
|
|
|
// resets task launch delay of the specific application
|
|
|
|
DeleteQueueDelay(appID string) error
|
|
|
|
|
|
|
|
// --- MISC ---
|
|
|
|
|
|
|
|
// get the marathon url
|
|
|
|
GetMarathonURL() string
|
|
|
|
// ping the marathon
|
|
|
|
Ping() (bool, error)
|
|
|
|
// grab the marathon server info
|
|
|
|
Info() (*Info, error)
|
|
|
|
// retrieve the leader info
|
|
|
|
Leader() (string, error)
|
|
|
|
// cause the current leader to abdicate
|
|
|
|
AbdicateLeader() (string, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
// ErrMarathonDown is thrown when all the marathon endpoints are down
|
|
|
|
ErrMarathonDown = errors.New("all the Marathon hosts are presently down")
|
|
|
|
// ErrTimeoutError is thrown when the operation has timed out
|
|
|
|
ErrTimeoutError = errors.New("the operation has timed out")
|
2017-12-19 15:00:09 +00:00
|
|
|
|
|
|
|
// Default HTTP client used for SSE subscription requests
|
|
|
|
// It is invalid to set client.Timeout because it includes time to read response so
|
|
|
|
// set dial, tls handshake and response header timeouts instead
|
|
|
|
defaultHTTPSSEClient = &http.Client{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
Dial: (&net.Dialer{
|
|
|
|
Timeout: 5 * time.Second,
|
|
|
|
}).Dial,
|
|
|
|
ResponseHeaderTimeout: 10 * time.Second,
|
|
|
|
TLSHandshakeTimeout: 5 * time.Second,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Default HTTP client used for non SSE requests
|
|
|
|
defaultHTTPClient = &http.Client{
|
|
|
|
Timeout: 10 * time.Second,
|
|
|
|
}
|
2017-02-07 21:33:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// EventsChannelContext holds contextual data for an EventsChannel.
|
|
|
|
type EventsChannelContext struct {
|
|
|
|
filter int
|
|
|
|
done chan struct{}
|
|
|
|
completion *sync.WaitGroup
|
|
|
|
}
|
|
|
|
|
|
|
|
type marathonClient struct {
|
|
|
|
sync.RWMutex
|
|
|
|
// the configuration for the client
|
|
|
|
config Config
|
|
|
|
// the flag used to prevent multiple SSE subscriptions
|
|
|
|
subscribedToSSE bool
|
|
|
|
// the ip address of the client
|
|
|
|
ipAddress string
|
|
|
|
// the http server
|
|
|
|
eventsHTTP *http.Server
|
|
|
|
// the marathon hosts
|
|
|
|
hosts *cluster
|
|
|
|
// a map of service you wish to listen to
|
|
|
|
listeners map[EventsChannel]EventsChannelContext
|
2017-12-19 15:00:09 +00:00
|
|
|
// a custom log function for debug messages
|
|
|
|
debugLog func(format string, v ...interface{})
|
2017-05-02 02:17:27 +00:00
|
|
|
// the marathon HTTP client to ensure consistency in requests
|
|
|
|
client *httpClient
|
|
|
|
}
|
|
|
|
|
|
|
|
type httpClient struct {
|
|
|
|
// the configuration for the marathon HTTP client
|
|
|
|
config Config
|
2017-02-07 21:33:23 +00:00
|
|
|
}
|
|
|
|
|
2017-05-19 12:24:28 +00:00
|
|
|
// newRequestError signals that creating a new http.Request failed
|
|
|
|
type newRequestError struct {
|
|
|
|
error
|
|
|
|
}
|
|
|
|
|
2017-02-07 21:33:23 +00:00
|
|
|
// NewClient creates a new marathon client
|
|
|
|
// config: the configuration to use
|
|
|
|
func NewClient(config Config) (Marathon, error) {
|
2017-12-19 15:00:09 +00:00
|
|
|
// step: if the SSE HTTP client is missing, prefer a configured regular
|
|
|
|
// client, and otherwise use the default SSE HTTP client.
|
|
|
|
if config.HTTPSSEClient == nil {
|
|
|
|
config.HTTPSSEClient = defaultHTTPSSEClient
|
|
|
|
if config.HTTPClient != nil {
|
|
|
|
config.HTTPSSEClient = config.HTTPClient
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// step: if a regular HTTP client is missing, use the default one.
|
2017-02-07 21:33:23 +00:00
|
|
|
if config.HTTPClient == nil {
|
2017-12-19 15:00:09 +00:00
|
|
|
config.HTTPClient = defaultHTTPClient
|
2017-02-07 21:33:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// step: if no polling wait time is set, default to 500 milliseconds.
|
|
|
|
if config.PollingWaitTime == 0 {
|
|
|
|
config.PollingWaitTime = defaultPollingWaitTime
|
|
|
|
}
|
|
|
|
|
2017-05-02 02:17:27 +00:00
|
|
|
// step: setup shared client
|
|
|
|
client := &httpClient{config: config}
|
|
|
|
|
2017-02-07 21:33:23 +00:00
|
|
|
// step: create a new cluster
|
2017-05-02 02:17:27 +00:00
|
|
|
hosts, err := newCluster(client, config.URL, config.DCOSToken != "")
|
2017-02-07 21:33:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-12-19 15:00:09 +00:00
|
|
|
debugLog := func(string, ...interface{}) {}
|
|
|
|
if config.LogOutput != nil {
|
|
|
|
logger := log.New(config.LogOutput, "", 0)
|
|
|
|
debugLog = func(format string, v ...interface{}) {
|
|
|
|
logger.Printf(format, v...)
|
|
|
|
}
|
2017-02-07 21:33:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return &marathonClient{
|
2017-05-02 02:17:27 +00:00
|
|
|
config: config,
|
|
|
|
listeners: make(map[EventsChannel]EventsChannelContext),
|
|
|
|
hosts: hosts,
|
2017-12-19 15:00:09 +00:00
|
|
|
debugLog: debugLog,
|
2017-05-02 02:17:27 +00:00
|
|
|
client: client,
|
2017-02-07 21:33:23 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetMarathonURL retrieves the marathon url
|
|
|
|
func (r *marathonClient) GetMarathonURL() string {
|
|
|
|
return r.config.URL
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ping pings the current marathon endpoint (note, this is not a ICMP ping, but a rest api call)
|
|
|
|
func (r *marathonClient) Ping() (bool, error) {
|
|
|
|
if err := r.apiGet(marathonAPIPing, nil, nil); err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2018-07-03 21:42:03 +00:00
|
|
|
func (r *marathonClient) apiHead(path string, result interface{}) error {
|
|
|
|
return r.apiCall("HEAD", path, nil, result)
|
|
|
|
}
|
|
|
|
|
2017-05-19 12:24:28 +00:00
|
|
|
func (r *marathonClient) apiGet(path string, post, result interface{}) error {
|
|
|
|
return r.apiCall("GET", path, post, result)
|
2017-02-07 21:33:23 +00:00
|
|
|
}
|
|
|
|
|
2017-05-19 12:24:28 +00:00
|
|
|
func (r *marathonClient) apiPut(path string, post, result interface{}) error {
|
|
|
|
return r.apiCall("PUT", path, post, result)
|
2017-02-07 21:33:23 +00:00
|
|
|
}
|
|
|
|
|
2017-05-19 12:24:28 +00:00
|
|
|
func (r *marathonClient) apiPost(path string, post, result interface{}) error {
|
|
|
|
return r.apiCall("POST", path, post, result)
|
2017-02-07 21:33:23 +00:00
|
|
|
}
|
|
|
|
|
2017-05-19 12:24:28 +00:00
|
|
|
func (r *marathonClient) apiDelete(path string, post, result interface{}) error {
|
|
|
|
return r.apiCall("DELETE", path, post, result)
|
2017-02-07 21:33:23 +00:00
|
|
|
}
|
|
|
|
|
2017-05-19 12:24:28 +00:00
|
|
|
func (r *marathonClient) apiCall(method, path string, body, result interface{}) error {
|
2018-07-03 21:42:03 +00:00
|
|
|
const deploymentHeader = "Marathon-Deployment-Id"
|
|
|
|
|
2017-02-07 21:33:23 +00:00
|
|
|
for {
|
|
|
|
// step: marshall the request to json
|
|
|
|
var requestBody []byte
|
2017-05-02 02:17:27 +00:00
|
|
|
var err error
|
2017-02-07 21:33:23 +00:00
|
|
|
if body != nil {
|
|
|
|
if requestBody, err = json.Marshal(body); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-02 02:17:27 +00:00
|
|
|
// step: create the API request
|
2017-05-19 12:24:28 +00:00
|
|
|
request, member, err := r.buildAPIRequest(method, path, bytes.NewReader(requestBody))
|
2017-02-07 21:33:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-05-02 02:17:27 +00:00
|
|
|
|
|
|
|
// step: perform the API request
|
|
|
|
response, err := r.client.Do(request)
|
2017-02-07 21:33:23 +00:00
|
|
|
if err != nil {
|
|
|
|
r.hosts.markDown(member)
|
|
|
|
// step: attempt the request on another member
|
2017-12-19 15:00:09 +00:00
|
|
|
r.debugLog("apiCall(): request failed on host: %s, error: %s, trying another", member, err)
|
2017-02-07 21:33:23 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
|
|
|
|
|
|
// step: read the response body
|
|
|
|
respBody, err := ioutil.ReadAll(response.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(requestBody) > 0 {
|
2017-12-19 15:00:09 +00:00
|
|
|
r.debugLog("apiCall(): %v %v %s returned %v %s", request.Method, request.URL.String(), requestBody, response.Status, oneLogLine(respBody))
|
2017-02-07 21:33:23 +00:00
|
|
|
} else {
|
2017-12-19 15:00:09 +00:00
|
|
|
r.debugLog("apiCall(): %v %v returned %v %s", request.Method, request.URL.String(), response.Status, oneLogLine(respBody))
|
2017-02-07 21:33:23 +00:00
|
|
|
}
|
|
|
|
|
2018-07-03 21:42:03 +00:00
|
|
|
// step: check for a successful response
|
2017-02-07 21:33:23 +00:00
|
|
|
if response.StatusCode >= 200 && response.StatusCode <= 299 {
|
|
|
|
if result != nil {
|
2018-07-03 21:42:03 +00:00
|
|
|
// If we have a deployment ID header and no response body, give them that
|
|
|
|
// This specifically handles the use case of a DELETE on an app/pod
|
|
|
|
// We need a way to retrieve the deployment ID
|
|
|
|
deploymentID := response.Header.Get(deploymentHeader)
|
|
|
|
if len(respBody) == 0 && deploymentID != "" {
|
|
|
|
d := DeploymentID{
|
|
|
|
DeploymentID: deploymentID,
|
|
|
|
}
|
|
|
|
if deployID, ok := result.(*DeploymentID); ok {
|
|
|
|
*deployID = d
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if err := json.Unmarshal(respBody, result); err != nil {
|
|
|
|
return fmt.Errorf("failed to unmarshal response from Marathon: %s", err)
|
|
|
|
}
|
2017-02-07 21:33:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// step: if the member node returns a >= 500 && <= 599 we should try another node?
|
|
|
|
if response.StatusCode >= 500 && response.StatusCode <= 599 {
|
|
|
|
// step: mark the host as down
|
|
|
|
r.hosts.markDown(member)
|
2017-12-19 15:00:09 +00:00
|
|
|
r.debugLog("apiCall(): request failed, host: %s, status: %d, trying another", member, response.StatusCode)
|
2017-02-07 21:33:23 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
return NewAPIError(response.StatusCode, respBody)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-03 21:42:03 +00:00
|
|
|
// wait waits until the provided function returns true (or times out)
|
|
|
|
func (r *marathonClient) wait(name string, timeout time.Duration, fn func(string) bool) error {
|
|
|
|
timer := time.NewTimer(timeout)
|
|
|
|
defer timer.Stop()
|
|
|
|
|
|
|
|
ticker := time.NewTicker(r.config.PollingWaitTime)
|
|
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
|
|
if fn(name) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-timer.C:
|
|
|
|
return ErrTimeoutError
|
|
|
|
case <-ticker.C:
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-19 12:24:28 +00:00
|
|
|
// buildAPIRequest creates a default API request.
|
|
|
|
// It fails when there is no available member in the cluster anymore or when the request can not be built.
|
2017-05-19 12:24:28 +00:00
|
|
|
func (r *marathonClient) buildAPIRequest(method, path string, reader io.Reader) (request *http.Request, member string, err error) {
|
2017-05-02 02:17:27 +00:00
|
|
|
// Grab a member from the cluster
|
|
|
|
member, err = r.hosts.getMember()
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", ErrMarathonDown
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build the HTTP request to Marathon
|
2017-12-19 15:00:09 +00:00
|
|
|
request, err = r.client.buildMarathonJSONRequest(method, member, path, reader)
|
2017-05-02 02:17:27 +00:00
|
|
|
if err != nil {
|
2017-05-19 12:24:28 +00:00
|
|
|
return nil, member, newRequestError{err}
|
2017-05-02 02:17:27 +00:00
|
|
|
}
|
|
|
|
return request, member, nil
|
|
|
|
}
|
|
|
|
|
2017-12-19 15:00:09 +00:00
|
|
|
// buildMarathonJSONRequest is like buildMarathonRequest but sets the
|
|
|
|
// Content-Type and Accept headers to application/json.
|
|
|
|
func (rc *httpClient) buildMarathonJSONRequest(method, member, path string, reader io.Reader) (request *http.Request, err error) {
|
|
|
|
req, err := rc.buildMarathonRequest(method, member, path, reader)
|
|
|
|
if err == nil {
|
|
|
|
req.Header.Add("Content-Type", "application/json")
|
|
|
|
req.Header.Add("Accept", "application/json")
|
|
|
|
}
|
|
|
|
|
|
|
|
return req, err
|
|
|
|
}
|
|
|
|
|
2017-05-19 12:24:28 +00:00
|
|
|
// buildMarathonRequest creates a new HTTP request and configures it according to the *httpClient configuration.
|
|
|
|
// The path must not contain a leading "/", otherwise buildMarathonRequest will panic.
|
2017-12-19 15:00:09 +00:00
|
|
|
func (rc *httpClient) buildMarathonRequest(method, member, path string, reader io.Reader) (request *http.Request, err error) {
|
2017-05-19 12:24:28 +00:00
|
|
|
if strings.HasPrefix(path, "/") {
|
|
|
|
panic(fmt.Sprintf("Path '%s' must not start with a leading slash", path))
|
|
|
|
}
|
|
|
|
|
2017-05-02 02:17:27 +00:00
|
|
|
// Create the endpoint URL
|
2017-05-19 12:24:28 +00:00
|
|
|
url := fmt.Sprintf("%s/%s", member, path)
|
2017-05-02 02:17:27 +00:00
|
|
|
|
|
|
|
// Instantiate an HTTP request
|
|
|
|
request, err = http.NewRequest(method, url, reader)
|
2017-02-07 21:33:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add any basic auth and the content headers
|
2017-05-02 02:17:27 +00:00
|
|
|
if rc.config.HTTPBasicAuthUser != "" && rc.config.HTTPBasicPassword != "" {
|
|
|
|
request.SetBasicAuth(rc.config.HTTPBasicAuthUser, rc.config.HTTPBasicPassword)
|
2017-02-07 21:33:23 +00:00
|
|
|
}
|
|
|
|
|
2017-05-02 02:17:27 +00:00
|
|
|
if rc.config.DCOSToken != "" {
|
|
|
|
request.Header.Add("Authorization", "token="+rc.config.DCOSToken)
|
2017-02-07 21:33:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return request, nil
|
|
|
|
}
|
|
|
|
|
2017-05-02 02:17:27 +00:00
|
|
|
func (rc *httpClient) Do(request *http.Request) (response *http.Response, err error) {
|
|
|
|
return rc.config.HTTPClient.Do(request)
|
|
|
|
}
|
|
|
|
|
2017-02-07 21:33:23 +00:00
|
|
|
var oneLogLineRegex = regexp.MustCompile(`(?m)^\s*`)
|
|
|
|
|
|
|
|
// oneLogLine removes indentation at the beginning of each line and
|
|
|
|
// escapes new line characters.
|
|
|
|
func oneLogLine(in []byte) []byte {
|
|
|
|
return bytes.Replace(oneLogLineRegex.ReplaceAll(in, nil), []byte("\n"), []byte("\\n "), -1)
|
|
|
|
}
|