/* Copyright 2014 Rohith All rights reserved. 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 ( "encoding/json" "errors" "fmt" "net/url" "time" ) var ( // ErrNoApplicationContainer is thrown when a container has been specified yet ErrNoApplicationContainer = errors.New("you have not specified a docker container yet") ) // Applications is a collection of applications type Applications struct { Apps []Application `json:"apps"` } // Application is the definition for an application in marathon type Application struct { ID string `json:"id,omitempty"` Cmd *string `json:"cmd,omitempty"` Args *[]string `json:"args,omitempty"` Constraints *[][]string `json:"constraints,omitempty"` Container *Container `json:"container,omitempty"` CPUs float64 `json:"cpus,omitempty"` Disk *float64 `json:"disk,omitempty"` Env *map[string]string `json:"env,omitempty"` Executor *string `json:"executor,omitempty"` HealthChecks *[]HealthCheck `json:"healthChecks,omitempty"` Instances *int `json:"instances,omitempty"` Mem *float64 `json:"mem,omitempty"` Tasks []*Task `json:"tasks,omitempty"` Ports []int `json:"ports"` RequirePorts *bool `json:"requirePorts,omitempty"` BackoffSeconds *float64 `json:"backoffSeconds,omitempty"` BackoffFactor *float64 `json:"backoffFactor,omitempty"` MaxLaunchDelaySeconds *float64 `json:"maxLaunchDelaySeconds,omitempty"` Deployments []map[string]string `json:"deployments,omitempty"` Dependencies []string `json:"dependencies"` TasksRunning int `json:"tasksRunning,omitempty"` TasksStaged int `json:"tasksStaged,omitempty"` TasksHealthy int `json:"tasksHealthy,omitempty"` TasksUnhealthy int `json:"tasksUnhealthy,omitempty"` User string `json:"user,omitempty"` UpgradeStrategy *UpgradeStrategy `json:"upgradeStrategy,omitempty"` Uris *[]string `json:"uris"` Version string `json:"version,omitempty"` VersionInfo *VersionInfo `json:"versionInfo,omitempty"` Labels *map[string]string `json:"labels,omitempty"` AcceptedResourceRoles []string `json:"acceptedResourceRoles,omitempty"` LastTaskFailure *LastTaskFailure `json:"lastTaskFailure,omitempty"` Fetch []Fetch `json:"fetch"` } // ApplicationVersions is a collection of application versions for a specific app in marathon type ApplicationVersions struct { Versions []string `json:"versions"` } // ApplicationVersion is the application version response from marathon type ApplicationVersion struct { Version string `json:"version"` } // VersionInfo is the application versioning details from marathon type VersionInfo struct { LastScalingAt string `json:"lastScalingAt,omitempty"` LastConfigChangeAt string `json:"lastConfigChangeAt,omitempty"` } // Fetch will download URI before task starts type Fetch struct { URI string `json:"uri"` Executable bool `json:"executable"` Extract bool `json:"extract"` Cache bool `json:"cache"` } // NewDockerApplication creates a default docker application func NewDockerApplication() *Application { application := new(Application) application.Container = NewDockerContainer() return application } // Name sets the name / ID of the application i.e. the identifier for this application func (r *Application) Name(id string) *Application { r.ID = validateID(id) return r } // Command sets the cmd of the application func (r *Application) Command(cmd string) *Application { r.Cmd = &cmd return r } // CPU set the amount of CPU shares per instance which is assigned to the application // cpu: the CPU shared (check Docker docs) per instance func (r *Application) CPU(cpu float64) *Application { r.CPUs = cpu return r } // Storage sets the amount of disk space the application is assigned, which for docker // application I don't believe is relevant // disk: the disk space in MB func (r *Application) Storage(disk float64) *Application { r.Disk = &disk return r } // AllTaskRunning checks to see if all the application tasks are running, i.e. the instances is equal // to the number of running tasks func (r *Application) AllTaskRunning() bool { if r.Instances == nil || *r.Instances == 0 { return true } if r.Tasks == nil { return false } if r.TasksRunning == *r.Instances { return true } return false } // DependsOn adds one or more dependencies for this application. Note, if you want to wait for // an application dependency to actually be UP, i.e. not just deployed, you need a health check // on the dependant app. // names: the application id(s) this application depends on func (r *Application) DependsOn(names ...string) *Application { if r.Dependencies == nil { r.Dependencies = make([]string, 0) } r.Dependencies = append(r.Dependencies, names...) return r } // Memory sets he amount of memory the application can consume per instance // memory: the amount of MB to assign func (r *Application) Memory(memory float64) *Application { r.Mem = &memory return r } // Count sets the number of instances of the application to run // count: the number of instances to run func (r *Application) Count(count int) *Application { r.Instances = &count return r } // AddArgs adds one or more arguments to the applications // arguments: the argument(s) you are adding func (r *Application) AddArgs(arguments ...string) *Application { if r.Args == nil { r.EmptyArgs() } args := *r.Args args = append(args, arguments...) r.Args = &args return r } // EmptyArgs explicitly empties arguments -- use this if you need to empty // arguments of an application that already has arguments set (setting args to nil will // keep the current value) func (r *Application) EmptyArgs() *Application { r.Args = &[]string{} return r } // AddConstraint adds a new constraint // constraints: the constraint definition, one constraint per array element func (r *Application) AddConstraint(constraints ...string) *Application { if r.Constraints == nil { r.EmptyConstraints() } c := *r.Constraints c = append(c, constraints) r.Constraints = &c return r } // EmptyConstraints explicitly empties constraints -- use this if you need to empty // constraints of an application that already has constraints set (setting constraints to nil will // keep the current value) func (r *Application) EmptyConstraints() *Application { r.Constraints = &[][]string{} return r } // AddLabel adds a label to the application // name: the name of the label // value: value for this label func (r *Application) AddLabel(name, value string) *Application { if r.Labels == nil { r.EmptyLabels() } (*r.Labels)[name] = value return r } // EmptyLabels explicitly empties the labels -- use this if you need to empty // the labels of an application that already has labels set (setting labels to nil will // keep the current value) func (r *Application) EmptyLabels() *Application { r.Labels = &map[string]string{} return r } // AddEnv adds an environment variable to the application // name: the name of the variable // value: go figure, the value associated to the above func (r *Application) AddEnv(name, value string) *Application { if r.Env == nil { r.EmptyEnvs() } (*r.Env)[name] = value return r } // EmptyEnvs explicitly empties the envs -- use this if you need to empty // the environments of an application that already has environments set (setting env to nil will // keep the current value) func (r *Application) EmptyEnvs() *Application { r.Env = &map[string]string{} return r } // SetExecutor sets the executor func (r *Application) SetExecutor(executor string) *Application { r.Executor = &executor return r } // AddHealthCheck adds a health check // healthCheck the health check that should be added func (r *Application) AddHealthCheck(healthCheck HealthCheck) *Application { if r.HealthChecks == nil { r.EmptyHealthChecks() } healthChecks := *r.HealthChecks healthChecks = append(healthChecks, healthCheck) r.HealthChecks = &healthChecks return r } // EmptyHealthChecks explicitly empties health checks -- use this if you need to empty // health checks of an application that already has health checks set (setting health checks to nil will // keep the current value) func (r *Application) EmptyHealthChecks() *Application { r.HealthChecks = &[]HealthCheck{} return r } // HasHealthChecks is a helper method, used to check if an application has health checks func (r *Application) HasHealthChecks() bool { return r.HealthChecks != nil && len(*r.HealthChecks) > 0 } // DeploymentIDs retrieves the application deployments IDs func (r *Application) DeploymentIDs() []*DeploymentID { var deployments []*DeploymentID if r.Deployments == nil { return deployments } // step: extract the deployment id from the result for _, deploy := range r.Deployments { if id, found := deploy["id"]; found { deployment := &DeploymentID{ Version: r.Version, DeploymentID: id, } deployments = append(deployments, deployment) } } return deployments } // CheckHTTP adds a HTTP check to an application // port: the port the check should be checking // interval: the interval in seconds the check should be performed func (r *Application) CheckHTTP(uri string, port, interval int) (*Application, error) { if r.Container == nil || r.Container.Docker == nil { return nil, ErrNoApplicationContainer } // step: get the port index portIndex, err := r.Container.Docker.ServicePortIndex(port) if err != nil { return nil, err } health := NewDefaultHealthCheck() health.IntervalSeconds = interval *health.Path = uri *health.PortIndex = portIndex // step: add to the checks r.AddHealthCheck(*health) return r, nil } // CheckTCP adds a TCP check to an application; note the port mapping must already exist, or an // error will thrown // port: the port the check should, err, check // interval: the interval in seconds the check should be performed func (r *Application) CheckTCP(port, interval int) (*Application, error) { if r.Container == nil || r.Container.Docker == nil { return nil, ErrNoApplicationContainer } // step: get the port index portIndex, err := r.Container.Docker.ServicePortIndex(port) if err != nil { return nil, err } health := NewDefaultHealthCheck() health.Protocol = "TCP" health.IntervalSeconds = interval *health.PortIndex = portIndex // step: add to the checks r.AddHealthCheck(*health) return r, nil } // AddUris adds one or more uris to the applications // arguments: the uri(s) you are adding func (r *Application) AddUris(newUris ...string) *Application { if r.Uris == nil { r.EmptyUris() } uris := *r.Uris uris = append(uris, newUris...) r.Uris = &uris return r } // EmptyUris explicitly empties uris -- use this if you need to empty // uris of an application that already has uris set (setting uris to nil will // keep the current value) func (r *Application) EmptyUris() *Application { r.Uris = &[]string{} return r } // String returns the json representation of this application func (r *Application) String() string { s, err := json.MarshalIndent(r, "", " ") if err != nil { return fmt.Sprintf(`{"error": "error decoding type into json: %s"}`, err) } return string(s) } // Applications retrieves an array of all the applications which are running in marathon func (r *marathonClient) Applications(v url.Values) (*Applications, error) { applications := new(Applications) err := r.apiGet(marathonAPIApps+"?"+v.Encode(), nil, applications) if err != nil { return nil, err } return applications, nil } // ListApplications retrieves an array of the application names currently running in marathon func (r *marathonClient) ListApplications(v url.Values) ([]string, error) { applications, err := r.Applications(v) if err != nil { return nil, err } var list []string for _, application := range applications.Apps { list = append(list, application.ID) } return list, nil } // HasApplicationVersion checks to see if the application version exists in Marathon // name: the id used to identify the application // version: the version (normally a timestamp) your looking for func (r *marathonClient) HasApplicationVersion(name, version string) (bool, error) { id := trimRootPath(name) versions, err := r.ApplicationVersions(id) if err != nil { return false, err } return contains(versions.Versions, version), nil } // ApplicationVersions is a list of versions which has been deployed with marathon for a specific application // name: the id used to identify the application func (r *marathonClient) ApplicationVersions(name string) (*ApplicationVersions, error) { uri := fmt.Sprintf("%s/versions", buildURI(name)) versions := new(ApplicationVersions) if err := r.apiGet(uri, nil, versions); err != nil { return nil, err } return versions, nil } // SetApplicationVersion changes the version of the application // name: the id used to identify the application // version: the version (normally a timestamp) you wish to change to func (r *marathonClient) SetApplicationVersion(name string, version *ApplicationVersion) (*DeploymentID, error) { uri := fmt.Sprintf(buildURI(name)) deploymentID := new(DeploymentID) if err := r.apiPut(uri, version, deploymentID); err != nil { return nil, err } return deploymentID, nil } // Application retrieves the application configuration from marathon // name: the id used to identify the application func (r *marathonClient) Application(name string) (*Application, error) { var wrapper struct { Application *Application `json:"app"` } if err := r.apiGet(buildURI(name), nil, &wrapper); err != nil { return nil, err } return wrapper.Application, nil } // ApplicationByVersion retrieves the application configuration from marathon // name: the id used to identify the application // version: the version of the configuration you would like to receive func (r *marathonClient) ApplicationByVersion(name, version string) (*Application, error) { var app *Application uri := fmt.Sprintf("%s/versions/%s", buildURI(name), version) if err := r.apiGet(uri, nil, &app); err != nil { return nil, err } return app, nil } // ApplicationOK validates that the application, or more appropriately it's tasks have passed all the health checks. // If no health checks exist, we simply return true // name: the id used to identify the application func (r *marathonClient) ApplicationOK(name string) (bool, error) { // step: get the application application, err := r.Application(name) if err != nil { return false, err } // step: check if all the tasks are running? if !application.AllTaskRunning() { return false, nil } // step: if the application has not health checks, just return true if application.HealthChecks == nil || len(*application.HealthChecks) == 0 { return true, nil } // step: iterate the application checks and look for false for _, task := range application.Tasks { for _, check := range task.HealthCheckResults { //When a task is flapping in Marathon, this is sometimes nil if check == nil || !check.Alive { return false, nil } } } return true, nil } // ApplicationDeployments retrieves an array of Deployment IDs for an application // name: the id used to identify the application func (r *marathonClient) ApplicationDeployments(name string) ([]*DeploymentID, error) { application, err := r.Application(name) if err != nil { return nil, err } return application.DeploymentIDs(), nil } // CreateApplication creates a new application in Marathon // application: the structure holding the application configuration func (r *marathonClient) CreateApplication(application *Application) (*Application, error) { result := new(Application) if err := r.apiPost(marathonAPIApps, &application, result); err != nil { return nil, err } return result, nil } // WaitOnApplication waits for an application to be deployed // name: the id of the application // timeout: a duration of time to wait for an application to deploy func (r *marathonClient) WaitOnApplication(name string, timeout time.Duration) error { if r.appExistAndRunning(name) { return nil } ticker := time.NewTicker(time.Millisecond * 500) defer ticker.Stop() for { select { case <-time.After(timeout): return ErrTimeoutError case <-ticker.C: if r.appExistAndRunning(name) { return nil } } } } func (r *marathonClient) appExistAndRunning(name string) bool { app, err := r.Application(name) if apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound { return false } if err == nil && app.AllTaskRunning() { return true } return false } // DeleteApplication deletes an application from marathon // name: the id used to identify the application func (r *marathonClient) DeleteApplication(name string) (*DeploymentID, error) { // step: check of the application already exists deployID := new(DeploymentID) if err := r.apiDelete(buildURI(name), nil, deployID); err != nil { return nil, err } return deployID, nil } // RestartApplication performs a rolling restart of marathon application // name: the id used to identify the application func (r *marathonClient) RestartApplication(name string, force bool) (*DeploymentID, error) { deployment := new(DeploymentID) var options struct { Force bool `json:"force"` } options.Force = force if err := r.apiPost(fmt.Sprintf("%s/restart", buildURI(name)), &options, deployment); err != nil { return nil, err } return deployment, nil } // ScaleApplicationInstances changes the number of instance an application is running // name: the id used to identify the application // instances: the number of instances you wish to change to // force: used to force the scale operation in case of blocked deployment func (r *marathonClient) ScaleApplicationInstances(name string, instances int, force bool) (*DeploymentID, error) { changes := new(Application) changes.ID = validateID(name) changes.Instances = &instances uri := buildURIWithForceParam(name, force) deployID := new(DeploymentID) if err := r.apiPut(uri, &changes, deployID); err != nil { return nil, err } return deployID, nil } // UpdateApplication updates an application in Marathon // application: the structure holding the application configuration func (r *marathonClient) UpdateApplication(application *Application, force bool) (*DeploymentID, error) { result := new(DeploymentID) uri := buildURIWithForceParam(application.ID, force) if err := r.apiPut(uri, &application, result); err != nil { return nil, err } return result, nil } func buildURIWithForceParam(path string, force bool) string { uri := buildURI(path) if force { uri += "?force=true" } return uri } func buildURI(path string) string { return fmt.Sprintf("%s/%s", marathonAPIApps, trimRootPath(path)) }