1/*
2Copyright 2014 The go-marathon Authors All rights reserved.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package marathon
18
19import (
20	"fmt"
21	"strings"
22)
23
24// Tasks is a collection of marathon tasks
25type Tasks struct {
26	Tasks []Task `json:"tasks"`
27}
28
29// Task is the definition for a marathon task
30type Task struct {
31	ID                 string               `json:"id"`
32	AppID              string               `json:"appId"`
33	Host               string               `json:"host"`
34	HealthCheckResults []*HealthCheckResult `json:"healthCheckResults"`
35	Ports              []int                `json:"ports"`
36	ServicePorts       []int                `json:"servicePorts"`
37	SlaveID            string               `json:"slaveId"`
38	StagedAt           string               `json:"stagedAt"`
39	StartedAt          string               `json:"startedAt"`
40	State              string               `json:"state"`
41	IPAddresses        []*IPAddress         `json:"ipAddresses"`
42	Version            string               `json:"version"`
43}
44
45// IPAddress represents a task's IP address and protocol.
46type IPAddress struct {
47	IPAddress string `json:"ipAddress"`
48	Protocol  string `json:"protocol"`
49}
50
51// AllTasksOpts contains a payload for AllTasks method
52//		status:		Return only those tasks whose status matches this parameter.
53//				If not specified, all tasks are returned. Possible values: running, staging. Default: none.
54type AllTasksOpts struct {
55	Status string `url:"status,omitempty"`
56}
57
58// KillApplicationTasksOpts contains a payload for KillApplicationTasks method
59//		host:		kill only those tasks on a specific host (optional)
60//		scale:		Scale the app down (i.e. decrement its instances setting by the number of tasks killed) after killing the specified tasks
61type KillApplicationTasksOpts struct {
62	Host  string `url:"host,omitempty"`
63	Scale bool   `url:"scale,omitempty"`
64	Force bool   `url:"force,omitempty"`
65}
66
67// KillTaskOpts contains a payload for task killing methods
68//		scale:		Scale the app down
69type KillTaskOpts struct {
70	Scale bool `url:"scale,omitempty"`
71	Force bool `url:"force,omitempty"`
72}
73
74// HasHealthCheckResults checks if the task has any health checks
75func (r *Task) HasHealthCheckResults() bool {
76	return r.HealthCheckResults != nil && len(r.HealthCheckResults) > 0
77}
78
79// AllTasks lists tasks of all applications.
80//		opts: 		AllTasksOpts request payload
81func (r *marathonClient) AllTasks(opts *AllTasksOpts) (*Tasks, error) {
82	path, err := addOptions(marathonAPITasks, opts)
83	if err != nil {
84		return nil, err
85	}
86
87	tasks := new(Tasks)
88	if err := r.apiGet(path, nil, tasks); err != nil {
89		return nil, err
90	}
91
92	return tasks, nil
93}
94
95// Tasks retrieves a list of tasks for an application
96//		id:		the id of the application
97func (r *marathonClient) Tasks(id string) (*Tasks, error) {
98	tasks := new(Tasks)
99	if err := r.apiGet(fmt.Sprintf("%s/%s/tasks", marathonAPIApps, trimRootPath(id)), nil, tasks); err != nil {
100		return nil, err
101	}
102
103	return tasks, nil
104}
105
106// KillApplicationTasks kills all tasks relating to an application
107//		id:		the id of the application
108//		opts: 		KillApplicationTasksOpts request payload
109func (r *marathonClient) KillApplicationTasks(id string, opts *KillApplicationTasksOpts) (*Tasks, error) {
110	path := fmt.Sprintf("%s/%s/tasks", marathonAPIApps, trimRootPath(id))
111	path, err := addOptions(path, opts)
112	if err != nil {
113		return nil, err
114	}
115
116	tasks := new(Tasks)
117	if err := r.apiDelete(path, nil, tasks); err != nil {
118		return nil, err
119	}
120
121	return tasks, nil
122}
123
124// KillTask kills the task associated with a given ID
125// 	taskID:		the id for the task
126//	opts:		KillTaskOpts request payload
127func (r *marathonClient) KillTask(taskID string, opts *KillTaskOpts) (*Task, error) {
128	appName := taskID[0:strings.LastIndex(taskID, ".")]
129	appName = strings.Replace(appName, "_", "/", -1)
130	taskID = strings.Replace(taskID, "/", "_", -1)
131
132	path := fmt.Sprintf("%s/%s/tasks/%s", marathonAPIApps, appName, taskID)
133	path, err := addOptions(path, opts)
134	if err != nil {
135		return nil, err
136	}
137
138	wrappedTask := new(struct {
139		Task Task `json:"task"`
140	})
141
142	if err := r.apiDelete(path, nil, wrappedTask); err != nil {
143		return nil, err
144	}
145
146	return &wrappedTask.Task, nil
147}
148
149// KillTasks kills tasks associated with given array of ids
150//	tasks:		the array of task ids
151//	opts:		KillTaskOpts request payload
152func (r *marathonClient) KillTasks(tasks []string, opts *KillTaskOpts) error {
153	path := fmt.Sprintf("%s/delete", marathonAPITasks)
154	path, err := addOptions(path, opts)
155	if err != nil {
156		return nil
157	}
158
159	var post struct {
160		IDs []string `json:"ids"`
161	}
162	post.IDs = tasks
163
164	return r.apiPost(path, &post, nil)
165}
166
167// TaskEndpoints gets the endpoints i.e. HOST_IP:DYNAMIC_PORT for a specific application service
168// I.e. a container running apache, might have ports 80/443 (translated to X dynamic ports), but i want
169// port 80 only and i only want those whom have passed the health check
170//
171// Note: I've NO IDEA how to associate the health_check_result to the actual port, I don't think it's
172// possible at the moment, however, given marathon will fail and restart an application even if one of x ports of a task is
173// down, the per port check is redundant??? .. personally, I like it anyhow, but hey
174//
175
176//		name:		the identifier for the application
177//		port:		the container port you are interested in
178//		health: 	whether to check the health or not
179func (r *marathonClient) TaskEndpoints(name string, port int, healthCheck bool) ([]string, error) {
180	// step: get the application details
181	application, err := r.Application(name)
182	if err != nil {
183		return nil, err
184	}
185
186	// step: we need to get the port index of the service we are interested in
187	portIndex, err := application.Container.Docker.ServicePortIndex(port)
188	if err != nil {
189		portIndex, err = application.Container.ServicePortIndex(port)
190		if err != nil {
191			return nil, err
192		}
193	}
194
195	// step: do we have any tasks?
196	if application.Tasks == nil || len(application.Tasks) == 0 {
197		return nil, nil
198	}
199
200	// step: if we are checking health the 'service' has a health check?
201	healthCheck = healthCheck && application.HasHealthChecks()
202
203	// step: iterate the tasks and extract the dynamic ports
204	var list []string
205	for _, task := range application.Tasks {
206		if !healthCheck || task.allHealthChecksAlive() {
207			endpoint := fmt.Sprintf("%s:%d", task.Host, task.Ports[portIndex])
208			list = append(list, endpoint)
209		}
210	}
211
212	return list, nil
213}
214
215func (r *Task) allHealthChecksAlive() bool {
216	// check: does the task have a health check result, if NOT, it's because the
217	// health of the task hasn't yet been performed, hence we assume it as DOWN
218	if !r.HasHealthCheckResults() {
219		return false
220	}
221	// step: check the health results then
222	for _, check := range r.HealthCheckResults {
223		if !check.Alive {
224			return false
225		}
226	}
227
228	return true
229}
230