├── .drone.yml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .pullapprove.yml ├── README.md ├── mesos-starter-sample ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── containersolutions │ │ └── mesos │ │ └── SampleApplication.java │ └── resources │ ├── application.yml │ └── banner.txt ├── mvnw ├── mvnw.cmd ├── pom.xml └── spring-boot-starter-mesos ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── containersolutions │ │ └── mesos │ │ ├── config │ │ ├── annotation │ │ │ └── EnableMesosScheduler.java │ │ ├── autoconfigure │ │ │ └── MesosSchedulerConfiguration.java │ │ └── validation │ │ │ └── MesosSchedulerPropertiesValidator.java │ │ ├── scheduler │ │ ├── CommandInfoMesosProtoFactory.java │ │ ├── CredentialFactory.java │ │ ├── ExecutionParameters.java │ │ ├── FrameworkInfoFactory.java │ │ ├── InstanceCount.java │ │ ├── MesosProtoFactory.java │ │ ├── OfferStrategyFilter.java │ │ ├── TaskInfoFactory.java │ │ ├── TaskInfoFactoryCommand.java │ │ ├── TaskInfoFactoryDocker.java │ │ ├── TaskMaterializer.java │ │ ├── TaskMaterializerMinimal.java │ │ ├── TaskProposal.java │ │ ├── TaskReaper.java │ │ ├── TeardownManager.java │ │ ├── UniversalScheduler.java │ │ ├── config │ │ │ ├── DockerConfigProperties.java │ │ │ ├── MesosConfigProperties.java │ │ │ ├── ResourcePortConfigProperties.java │ │ │ ├── ResourceVolumeConfigProperties.java │ │ │ ├── ResourcesConfigProperties.java │ │ │ └── ZookeeperConfigProperties.java │ │ ├── events │ │ │ ├── ErrorEvent.java │ │ │ ├── ExecutorLostEvent.java │ │ │ ├── FrameworkMessageEvent.java │ │ │ ├── FrameworkRegistreredEvent.java │ │ │ ├── FrameworkRemovedEvent.java │ │ │ ├── FrameworkReregistreredEvent.java │ │ │ ├── InstanceCountChangeEvent.java │ │ │ ├── MesosEvent.java │ │ │ ├── SlaveLostEvent.java │ │ │ ├── StatusUpdateEvent.java │ │ │ └── TearDownFrameworkEvent.java │ │ ├── requirements │ │ │ ├── DistinctSlaveRequirement.java │ │ │ ├── InstancesCountRequirement.java │ │ │ ├── OfferEvaluation.java │ │ │ ├── PortMapping.java │ │ │ ├── PortsRequirement.java │ │ │ ├── ResourceRequirement.java │ │ │ ├── RoleRequirement.java │ │ │ ├── VolumeMapping.java │ │ │ └── VolumesRequirement.java │ │ └── state │ │ │ ├── StateRepository.java │ │ │ ├── StateRepositoryFile.java │ │ │ └── StateRepositoryZookeeper.java │ │ └── utils │ │ ├── MesosHelper.java │ │ └── StreamHelper.java └── resources │ └── META-INF │ └── spring.factories └── test └── java └── com └── containersolutions └── mesos ├── TestHelper.java ├── config └── validation │ └── MesosSchedulerPropertiesValidatorTest.java └── scheduler ├── FrameworkInfoFactoryTest.java ├── OfferStrategyFilterTest.java ├── TaskReaperTest.java ├── UniversalSchedulerTest.java └── requirements ├── DistinctSlaveRequirementTest.java ├── InstancesCountRequirementTest.java └── PortsRequirementTest.java /.drone.yml: -------------------------------------------------------------------------------- 1 | pipeline: 2 | publish: 3 | image: maven:3.5-jdk-9 4 | environment: 5 | - MAVEN_USERNAME=drone 6 | secrets: 7 | - maven_password 8 | commands: 9 | - mkdir -p ~/.m2 10 | - > 11 | echo ' 12 | 13 | 20 | 21 | 22 | humio-releases 23 | $${MAVEN_USERNAME} 24 | $${MAVEN_PASSWORD} 25 | 26 | 27 | humio-public 28 | $${MAVEN_USERNAME} 29 | $${MAVEN_PASSWORD} 30 | 31 | 32 | ' > ~/.m2/settings.xml 33 | - mvn -B versions:set versions:commit -DnewVersion=build-${DRONE_BUILD_NUMBER} 34 | - mvn -B deploy 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | 3 | *.iml 4 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContainerSolutions/mesos-starter/22fb58bf10fc6c726b34117220d54def141beacc/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip -------------------------------------------------------------------------------- /.pullapprove.yml: -------------------------------------------------------------------------------- 1 | approve_by_comment: true 2 | approve_regex: '^(Approved|LGTM)' 3 | reject_regex: ^Rejected 4 | reset_on_push: false 5 | reviewers: 6 | - 7 | name: admins 8 | members: 9 | - mwl 10 | required: 1 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot starter for Mesos 2 | 3 | [![Join the chat at https://gitter.im/ContainerSolutions/mesos-starter](https://badges.gitter.im/ContainerSolutions/mesos-starter.svg)](https://gitter.im/ContainerSolutions/mesos-starter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Spring Boot starter package for writing Mesos frameworks 6 | 7 | ## Features 8 | - Vertical scaling 9 | - Deploy executor on all slaves 10 | - Support for Docker containers 11 | 12 | ## Getting Started 13 | Start by adding the `spring-boot-starter-mesos` dependency to your project 14 | 15 | ``` 16 | 17 | com.github.containersolutions.mesos-starter 18 | spring-boot-starter-mesos 19 | 0.1 20 | 21 | ``` 22 | 23 | Make sure your Spring Boot application has a name, by adding the `spring.application.name` to the `application.properties` file i.e. 24 | 25 | ``` 26 | spring.application.name=Sample application 27 | ``` 28 | 29 | To run three instances of a Docker image add the following in `application.properties` 30 | 31 | ``` 32 | mesos.resources.distinctSlave=true 33 | mesos.resources.count=3 34 | mesos.resources.cpus=0.1 35 | mesos.resources.mem=64 36 | mesos.docker.image=tutum/hello-world:latest 37 | ``` 38 | 39 | That is all you need to do if you have annotated your Spring configuration with `@SpringBootApplication` or `@EnableAutoConfiguration`. 40 | 41 | ``` 42 | @SpringBootApplication 43 | public class SampleApplication { 44 | 45 | public static void main(String[] args) { 46 | SpringApplication.run(SampleApplication.class, args); 47 | } 48 | } 49 | ``` 50 | 51 | For a complete example, see `mesos-starter-sample` module. 52 | 53 | ## Starting the application 54 | The only required parameter for the scheduler is `mesos.master`. The value of the parameter is passed directly to the Mesos Scheduler Driver which allows the following formats 55 | 56 | - `host:port` 57 | - `zk://host1:port1,host2:port2,.../path` 58 | - `zk://username:password@host1:port1,host2:port2,.../path` 59 | - `file:///path/to/file` 60 | 61 | For resiliency it is also recommended to point the application to a Zookeeper cluster and giving the instance a name. 62 | ``` 63 | mesos.zookeeper.server=zookeeper:2181 64 | mesos.framework.name=sampleApp1 65 | ``` 66 | 67 | The purpose of `mesos.framework.name` is to distinguish instances of the scheduler. 68 | 69 | ## Running tasks 70 | Currently only Docker and shell commands are supported. 71 | 72 | ### Docker 73 | Run any Docker image by setting the `mesos.docker.image` property. Eventually by overriding the `CMD` by `mesos.docker.command` 74 | 75 | ### Shell command 76 | Run a command on by setting the `mesos.shell.command` property. 77 | 78 | ### Custom task 79 | Extend the `TaskInfoFactory` class to create your own task. 80 | 81 | # Offers evaluation 82 | Mesos-starter offers a set of offer evaluation rules 83 | - Physical requirements 84 | - Distinct slave 85 | - Instances count 86 | - Role assigned 87 | 88 | They all work in combination with each other, though this might change in the future. 89 | 90 | ## Physical requirements 91 | Reject offers that does not have the required amount of either CPUs, memory or ports. 92 | 93 | ### Ports 94 | The ports requirement serves two purposes. Grapping ports and mapping them to the application. 95 | A very base configuration looks like this 96 | ``` 97 | mesos.resources.ports.http.host=ANY 98 | mesos.command=runwebserver.sh --port=$HTTP 99 | ``` 100 | This will grap any unprivileged port, and expose it in an environment variable. Value of `.host` can be any off 101 | - `ANY`, `UNPRIVILEGED` will reserve any any port above 1024 102 | - `PRIVILEGED` will only reserve a port below 1024 (included) 103 | - *Any positive number* will only reserve a fixed port. 104 | 105 | When running with containers you can add the `.container` to map it to a container port when running in bridge mode, i.e. 106 | 107 | ``` 108 | mesos.resources.ports.http.host=ANY 109 | mesos.resources.ports.http.container=80 110 | mesos.docker.network=BRIDGE 111 | mesos.docker.image=tutum/hello-world 112 | ``` 113 | 114 | This will reserve any port above 1024 and let docker map it to port 80 on the container. 115 | 116 | ## Distinct slave 117 | This rule will make sure that offers for hosts where the application is already running are being rejected. 118 | 119 | ## Instances count 120 | This rule will make sure that only a certain number of instances are running in the Mesos cluster. The instances count is exposed as a managed bean that can be accessed through Actuator Management API. Furthermore it'll also be possible to insert your own instances count bean. 121 | It is recommended to have this rule enabled in most cases. 122 | 123 | ## Role assigned 124 | This rule only accept offers assigned to the Role defined in `mesos.role`. 125 | 126 | ## Framework Authentication/Authorisation 127 | To use [Framework Authentication](http://mesos.apache.org/documentation/latest/authentication/), please pass the following settings: 128 | 129 | | Command | Description | Default | Required | 130 | | --- | --- | --- | --- | 131 | | mesos.principal | The Mesos principal | | | 132 | | mesos.secret | The Mesos secret | | | 133 | 134 | # Use cases 135 | 136 | A few good examples 137 | 138 | ## Stateless web application 139 | For a stateless web application that can run anywhere in the cluster with only a requirement for a single network port, the following should be sufficient 140 | 141 | ``` 142 | mesos.resources.count=3 143 | mesos.resources.cpus=0.1 144 | mesos.resources.mem=64 145 | mesos.resources.ports=1 146 | ``` 147 | 148 | This will run 3 instances of the application with one port exposed. Bare in mind that they all might run on the very same host. 149 | 150 | ## Distributed database application 151 | For a distributed database you want to run a certain number of instances and never more than one on every host. To achieve that you can enable `count` and `distinctSlave`, like 152 | 153 | ``` 154 | mesos.resources.count=3 155 | mesos.resources.distinctSlave=true 156 | mesos.resources.cpus=0.1 157 | mesos.resources.mem=64 158 | mesos.resources.ports=1 159 | ``` 160 | 161 | ## Cluster wide system daemon 162 | Often operations would like to run a single application on each host in the cluster to harvest information from every single node. This can be achieved by not adding the instances count rule and adding the Distinct slave rule. 163 | 164 | ``` 165 | mesos.resources.distinctSlave=true 166 | mesos.resources.cpus=0.1 167 | mesos.resources.mem=64 168 | ``` 169 | 170 | Another, safer, way to achieve the same result is by assigning resources to a specific role on all nodes. I.e. by adding the following to `/etc/mesos-slave/resources` 171 | ``` 172 | cpus(sampleDaemon):0.2; mem(sampleDaemon):64; ports(sampleDaemon):[514-514]; 173 | ``` 174 | 175 | And configure the scheduler with the following options 176 | 177 | ``` 178 | mesos.role=sampleDaemon 179 | mesos.resources.distinctSlave=true 180 | mesos.resources.role=all 181 | ``` 182 | 183 | This way the scheduler will take all resources allocated to the role and make sure it's only running once in every single slave. 184 | 185 | It is always recommended to run the scheduler with a Mesos role and reserved resources in such cases to make sure that scheduler is being offered resources for all nodes in the cluster. 186 | 187 | ### Framework shutdown 188 | The scheduler can survive `SIGKILL` or being lost (system crashes etc.). If you want to completely de-register the framework and shutdown all tasks, just stop the scheduler with a plain `SIGTERM`. 189 | -------------------------------------------------------------------------------- /mesos-starter-sample/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.containersolutions.mesos 9 | mesos-starter 10 | 1.0-SNAPSHOT 11 | 12 | 13 | mesos-starter-sample 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter 19 | 20 | 21 | ${project.groupId} 22 | spring-boot-starter-mesos 23 | ${project.version} 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /mesos-starter-sample/src/main/java/com/containersolutions/mesos/SampleApplication.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SampleApplication { 8 | public static void main(String[] args) { 9 | SpringApplication.run(SampleApplication.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mesos-starter-sample/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: Sample App 4 | 5 | mesos: 6 | command: sleep 600 7 | resources: 8 | # distinctSlave: true 9 | count: 1 10 | cpus: 0.1 11 | mem: 64 12 | # ports: 13 | # nc-port: 14 | # host: 31101 15 | # container: 80 16 | # volumes: 17 | # - hostPath: /etc 18 | # containerPath: /hostetc 19 | docker: 20 | # image: tutum/hello-world:latest 21 | image: alpine 22 | -------------------------------------------------------------------------------- /mesos-starter-sample/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ${AnsiColor.BRIGHT_BLUE} _____ _________ __ __ ${AnsiColor.DEFAULT} .__ 2 | ${AnsiColor.BRIGHT_BLUE} / \ ____ __________ ______ / _____// |______ ________/ |_ ___________ ${AnsiColor.DEFAULT} ___________ _____ ______ | | ____ 3 | ${AnsiColor.BRIGHT_BLUE} / \ / \_/ __ \ / ___/ _ \/ ___/ ______ \_____ \\ __\__ \\_ __ \ __\/ __ \_ __ \${AnsiColor.DEFAULT} / ___/\__ \ / \\____ \| | _/ __ \ 4 | ${AnsiColor.BRIGHT_BLUE}/ Y \ ___/ \___ ( <_> )___ \ /_____/ / \| | / __ \| | \/| | \ ___/| | \/${AnsiColor.DEFAULT} \___ \ / __ \| Y Y \ |_> > |_\ ___/ 5 | ${AnsiColor.BRIGHT_BLUE}\____|__ /\___ >____ >____/____ > /_______ /|__| (____ /__| |__| \___ >__| ${AnsiColor.DEFAULT} /____ >(____ /__|_| / __/|____/\___ > 6 | ${AnsiColor.BRIGHT_BLUE} \/ \/ \/ \/ \/ \/ \/ ${AnsiColor.DEFAULT} \/ \/ \/|__| \/ 7 | :: Running Spring Boot ${spring-boot.version} ::${AnsiColor.BLACK} -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # For Cygwin, switch paths to Windows format before running java 188 | if $cygwin; then 189 | [ -n "$M2_HOME" ] && 190 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 191 | [ -n "$JAVA_HOME" ] && 192 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 193 | [ -n "$CLASSPATH" ] && 194 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 195 | fi 196 | 197 | # traverses directory structure from process work directory to filesystem root 198 | # first directory with .mvn subdirectory is considered project base directory 199 | find_maven_basedir() { 200 | local basedir=$(pwd) 201 | local wdir=$(pwd) 202 | while [ "$wdir" != '/' ] ; do 203 | if [ -d "$wdir"/.mvn ] ; then 204 | basedir=$wdir 205 | break 206 | fi 207 | wdir=$(cd "$wdir/.."; pwd) 208 | done 209 | echo "${basedir}" 210 | } 211 | 212 | # concatenates all lines of a file 213 | concat_lines() { 214 | if [ -f "$1" ]; then 215 | echo "$(tr -s '\n' ' ' < "$1")" 216 | fi 217 | } 218 | 219 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 220 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 221 | 222 | # Provide a "standardized" way to retrieve the CLI args that will 223 | # work with both Windows and non-Windows executions. 224 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 225 | export MAVEN_CMD_LINE_ARGS 226 | 227 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 228 | 229 | exec "$JAVACMD" \ 230 | $MAVEN_OPTS \ 231 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 232 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 233 | ${WRAPPER_LAUNCHER} $MAVEN_CMD_LINE_ARGS 234 | 235 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | set MAVEN_CMD_LINE_ARGS=%MAVEN_CONFIG% %* 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | 121 | set WRAPPER_JAR=""%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% 125 | if ERRORLEVEL 1 goto error 126 | goto end 127 | 128 | :error 129 | set ERROR_CODE=1 130 | 131 | :end 132 | @endlocal & set ERROR_CODE=%ERROR_CODE% 133 | 134 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 135 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 136 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 137 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 138 | :skipRcPost 139 | 140 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 141 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 142 | 143 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 144 | 145 | exit /B %ERROR_CODE% 146 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.containersolutions.mesos 8 | mesos-starter 9 | 1.0-SNAPSHOT 10 | pom 11 | 12 | Mesos starter 13 | Spring Boot starter package for Mesos 14 | https://github.com/containersolutions/mesos-starter 15 | 16 | 17 | The Apache License, Version 2.0 18 | http://www.apache.org/licenses/LICENSE-2.0.txt 19 | 20 | 21 | 22 | 23 | Martin Westergaard Lassen 24 | martin@mwl.dk 25 | 26 | 27 | 28 | scm:git:git@github.com:containersolutions/mesos-starter.git 29 | scm:git:git@github.com:containersolutions/mesos-starter.git 30 | git@github.com:containersolutions/mesos-starter.git 31 | 32 | 33 | 34 | true 35 | 36 | 37 | 38 | spring-boot-starter-mesos 39 | mesos-starter-sample 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-dependencies 48 | 2.0.4.RELEASE 49 | pom 50 | import 51 | 52 | 53 | 54 | 55 | 56 | 57 | humio-releases 58 | Humio releases 59 | https://repo.humio.com/repository/maven-releases/ 60 | 61 | 62 | humio-snapshots 63 | Humio snapshots 64 | https://repo.humio.com/repository/maven-snapshots/ 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-compiler-plugin 73 | 3.3 74 | true 75 | 76 | 1.8 77 | 1.8 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-javadoc-plugin 83 | 2.10.3 84 | true 85 | 86 | 87 | 88 | jar 89 | 90 | 91 | 92 | 93 | 94 | org.apache.maven.plugins 95 | maven-source-plugin 96 | 3.0.0 97 | true 98 | 99 | 100 | 101 | jar 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.containersolutions.mesos 9 | mesos-starter 10 | 1.0-SNAPSHOT 11 | 12 | 13 | spring-boot-starter-mesos 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-configuration-processor 23 | 24 | 25 | org.apache.mesos 26 | mesos 27 | 1.5.0 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-test 34 | test 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/config/annotation/EnableMesosScheduler.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.config.annotation; 2 | 3 | import com.containersolutions.mesos.config.autoconfigure.MesosSchedulerConfiguration; 4 | import org.springframework.context.annotation.Import; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | @Target(ElementType.TYPE) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Import(MesosSchedulerConfiguration.class) 14 | public @interface EnableMesosScheduler { 15 | } 16 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/config/autoconfigure/MesosSchedulerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.config.autoconfigure; 2 | 3 | import com.containersolutions.mesos.config.validation.MesosSchedulerPropertiesValidator; 4 | import com.containersolutions.mesos.scheduler.*; 5 | import com.containersolutions.mesos.scheduler.config.MesosConfigProperties; 6 | import com.containersolutions.mesos.scheduler.requirements.*; 7 | import com.containersolutions.mesos.scheduler.state.StateRepository; 8 | import com.containersolutions.mesos.scheduler.state.StateRepositoryFile; 9 | import com.containersolutions.mesos.scheduler.state.StateRepositoryZookeeper; 10 | import org.apache.mesos.Protos; 11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 13 | import org.springframework.context.ApplicationEventPublisher; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Configuration; 16 | import org.springframework.core.Ordered; 17 | import org.springframework.core.annotation.Order; 18 | import org.springframework.core.env.Environment; 19 | 20 | import java.time.Clock; 21 | import java.util.Collections; 22 | import java.util.Map; 23 | import java.util.UUID; 24 | import java.util.concurrent.atomic.AtomicMarkableReference; 25 | import java.util.function.Supplier; 26 | 27 | @Configuration 28 | public class MesosSchedulerConfiguration { 29 | 30 | @Bean 31 | public Clock systemClock() { 32 | return Clock.systemUTC(); 33 | } 34 | 35 | @Bean 36 | public MesosSchedulerPropertiesValidator configurationPropertiesValidator() { 37 | return new MesosSchedulerPropertiesValidator(); 38 | } 39 | 40 | @Bean 41 | @ConditionalOnMissingBean 42 | public UniversalScheduler scheduler(MesosConfigProperties mesosConfig, OfferStrategyFilter offerStrategyFilter, ApplicationEventPublisher applicationEventPublisher, Supplier uuidSupplier, StateRepository stateRepository, TaskMaterializer taskMaterializer, FrameworkInfoFactory frameworkInfoFactory, CredentialFactory credentialFactory) { 43 | return new UniversalScheduler(mesosConfig, offerStrategyFilter, applicationEventPublisher, uuidSupplier, stateRepository, taskMaterializer, frameworkInfoFactory, credentialFactory); 44 | } 45 | 46 | @Bean 47 | public OfferStrategyFilter offerStrategyFilter(Map resourceRequirements) { 48 | return new OfferStrategyFilter(resourceRequirements); 49 | } 50 | 51 | private ResourceRequirement simpleScalarRequirement(String name, double minimumRequirement) { 52 | return (requirement, taskId, offer) -> { 53 | if (ResourceRequirement.scalarSum(offer, name) > minimumRequirement) { 54 | return OfferEvaluation.accept( 55 | requirement, 56 | taskId, 57 | offer, 58 | Collections.emptyMap(), 59 | Collections.emptyList(), 60 | Collections.emptyList(), 61 | Protos.Resource.newBuilder() 62 | .setType(Protos.Value.Type.SCALAR) 63 | .setName(name) 64 | .setRole(offer.getResourcesList().stream().filter(resource -> resource.getName().equalsIgnoreCase(name)).findFirst().map(Protos.Resource::getRole).orElse("*")) 65 | .setScalar(Protos.Value.Scalar.newBuilder().setValue(minimumRequirement)) 66 | .build() 67 | ); 68 | } 69 | return OfferEvaluation.decline(requirement, taskId, offer, "Not enough resources for " + name); 70 | }; 71 | 72 | } 73 | 74 | @Bean 75 | public AtomicMarkableReference frameworkId() { 76 | return new AtomicMarkableReference<>(Protos.FrameworkID.newBuilder().setValue("").build(), false); 77 | } 78 | 79 | @Bean 80 | @ConditionalOnProperty(prefix = "mesos.state.file", name = "location") 81 | public StateRepository stateRepositoryFile() { 82 | return new StateRepositoryFile(); 83 | } 84 | 85 | @Bean(initMethod = "connect") 86 | @ConditionalOnMissingBean(StateRepository.class) 87 | public StateRepositoryZookeeper stateRepositoryZookeeper(Environment environment) { 88 | return new StateRepositoryZookeeper(environment); 89 | } 90 | 91 | @Bean 92 | public Supplier uuidSupplier() { 93 | return UUID::randomUUID; 94 | } 95 | 96 | @Bean 97 | public MesosConfigProperties mesosConfig() { 98 | return new MesosConfigProperties(); 99 | } 100 | 101 | @Bean 102 | @ConditionalOnMissingBean(name = "commandInfoMesosProtoFactory") 103 | public MesosProtoFactory> commandInfoMesosProtoFactory(MesosConfigProperties mesosConfig) { 104 | return new CommandInfoMesosProtoFactory(mesosConfig); 105 | } 106 | 107 | @Bean 108 | @ConditionalOnMissingBean(TaskInfoFactory.class) 109 | @ConditionalOnProperty(prefix = "mesos.docker", name = {"image"}) 110 | public TaskInfoFactory taskInfoFactoryDocker(MesosConfigProperties mesosConfig, MesosProtoFactory> commandInfoMesosProtoFactory) { 111 | return new TaskInfoFactoryDocker(mesosConfig, commandInfoMesosProtoFactory); 112 | } 113 | 114 | @Bean 115 | @ConditionalOnMissingBean(TaskInfoFactory.class) 116 | public TaskInfoFactory taskInfoFactoryCommand(MesosProtoFactory> commandInfoMesosProtoFactory, Supplier uuidSupplier) { 117 | return new TaskInfoFactoryCommand(commandInfoMesosProtoFactory, uuidSupplier); 118 | } 119 | 120 | @Bean 121 | @ConditionalOnMissingBean(name = "distinctHostRequirement") 122 | @ConditionalOnProperty(prefix = "mesos.resources", name = "distinctSlave", havingValue = "true") 123 | @Order(Ordered.LOWEST_PRECEDENCE) 124 | public ResourceRequirement distinctHostRequirement(Clock systemClock, StateRepository stateRepository) { 125 | return new DistinctSlaveRequirement(systemClock, stateRepository); 126 | } 127 | 128 | @Bean 129 | @ConditionalOnMissingBean(name = "instancesCountRequirement") 130 | @ConditionalOnProperty(prefix = "mesos.resources", name = "count") 131 | @Order(Ordered.LOWEST_PRECEDENCE) 132 | public ResourceRequirement instancesCountRequirement(StateRepository stateRepository, InstanceCount instanceCount) { 133 | return new InstancesCountRequirement(stateRepository, instanceCount); 134 | } 135 | 136 | @Bean 137 | @ConditionalOnProperty(prefix = "mesos.resources", name = "count") 138 | public TaskReaper taskReaper(StateRepository stateRepository, InstanceCount instanceCount, UniversalScheduler universalScheduler) { 139 | return new TaskReaper(stateRepository, instanceCount, universalScheduler); 140 | } 141 | 142 | @Bean 143 | @ConditionalOnProperty(prefix = "mesos.resources", name = "count") 144 | public InstanceCount instanceCount(Environment env) { 145 | return new InstanceCount(env.getProperty("mesos.resources.count", Integer.class, 1)); 146 | } 147 | 148 | @Bean 149 | @ConditionalOnMissingBean(name = "roleRequirement") 150 | @ConditionalOnProperty(prefix = "mesos.resources", name = "role", havingValue = "all") 151 | @Order(Ordered.LOWEST_PRECEDENCE) 152 | public ResourceRequirement roleRequirement(MesosConfigProperties mesosConfig) { 153 | return new RoleRequirement(mesosConfig); 154 | } 155 | 156 | 157 | @Bean 158 | @ConditionalOnMissingBean(name = "cpuRequirement") 159 | @ConditionalOnProperty(prefix = "mesos.resources", name = "cpus") 160 | @Order(Ordered.HIGHEST_PRECEDENCE) 161 | public ResourceRequirement cpuRequirement(MesosConfigProperties mesosConfigProperties) { 162 | return simpleScalarRequirement("cpus", mesosConfigProperties.getResources().getCpus()); 163 | } 164 | 165 | @Bean 166 | @ConditionalOnMissingBean(name = "memRequirement") 167 | @ConditionalOnProperty(prefix = "mesos.resources", name = "mem") 168 | @Order(Ordered.HIGHEST_PRECEDENCE) 169 | public ResourceRequirement memRequirement(Environment environment) { 170 | return simpleScalarRequirement("mem", environment.getRequiredProperty("mesos.resources.mem", Double.class)); 171 | } 172 | 173 | @Bean 174 | @ConditionalOnMissingBean(name = "portsRequirement") 175 | // @ConditionalOnProperty(prefix = "mesos.resources", name = "ports") 176 | @Order(Ordered.HIGHEST_PRECEDENCE) 177 | public ResourceRequirement portsRequirement(MesosConfigProperties mesosConfig) { 178 | return new PortsRequirement(mesosConfig); 179 | } 180 | 181 | @Bean 182 | @ConditionalOnMissingBean(name = "volumesRequirements") 183 | @Order(Ordered.HIGHEST_PRECEDENCE) 184 | public ResourceRequirement volumesRequirement(MesosConfigProperties mesosConfig) { 185 | return new VolumesRequirement(mesosConfig); 186 | } 187 | 188 | @Bean 189 | public TaskMaterializer taskMaterializer(TaskInfoFactory taskInfoFactory) { 190 | return new TaskMaterializerMinimal(taskInfoFactory); 191 | } 192 | 193 | @Bean 194 | @ConditionalOnMissingBean 195 | public FrameworkInfoFactory frameworkInfoFactory(MesosConfigProperties mesosConfig, StateRepository stateRepository, CredentialFactory credentialFactory) { 196 | return new FrameworkInfoFactory(mesosConfig, stateRepository, credentialFactory); 197 | } 198 | 199 | @Bean 200 | @ConditionalOnMissingBean 201 | public CredentialFactory credentialFactory(MesosConfigProperties mesosConfig) { 202 | return new CredentialFactory(mesosConfig); 203 | } 204 | 205 | @Bean 206 | public TeardownManager teardownManager() { 207 | return new TeardownManager(); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/config/validation/MesosSchedulerPropertiesValidator.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.config.validation; 2 | 3 | import com.containersolutions.mesos.scheduler.config.MesosConfigProperties; 4 | import com.containersolutions.mesos.scheduler.config.ResourcesConfigProperties; 5 | import org.springframework.validation.Errors; 6 | import org.springframework.validation.Validator; 7 | 8 | import static org.springframework.util.StringUtils.isEmpty; 9 | import static org.springframework.validation.ValidationUtils.rejectIfEmptyOrWhitespace; 10 | 11 | public class MesosSchedulerPropertiesValidator implements Validator { 12 | @Override 13 | public boolean supports(Class clazz) { 14 | return clazz == MesosConfigProperties.class; 15 | } 16 | 17 | @Override 18 | public void validate(Object target, Errors errors) { 19 | if (target instanceof MesosConfigProperties) { 20 | MesosConfigProperties config = (MesosConfigProperties) target; 21 | 22 | rejectIfEmptyOrWhitespace(errors, "master", "master.empty", "No Mesos Master set"); 23 | rejectIfEmptyOrWhitespace(errors, "zookeeper.server", "zookeeper.server.empty", ""); 24 | 25 | validateResources(errors, config.getResources(), isContainerized(config) && config.getDocker().getNetwork().equalsIgnoreCase("bridge")); 26 | } 27 | } 28 | 29 | private boolean isContainerized(MesosConfigProperties config) { 30 | return config.getDocker() != null && !isEmpty(config.getDocker().getImage()); 31 | } 32 | 33 | private void validateResources(Errors errors, ResourcesConfigProperties resources, boolean containerizedNetwork) { 34 | if (resources == null) { 35 | errors.rejectValue("resources", "resources.empty", "Resources are not set"); 36 | return; 37 | } 38 | 39 | if (resources.getCount() < 0) { 40 | errors.rejectValue("resources.count", "resources.count.not_positive", "Count property must be a positive number"); 41 | } 42 | 43 | if (resources.getCpus() <= 0.0) { 44 | errors.rejectValue("resources.cpus", "resources.cpus.not_positive", "cpus must be a positive number"); 45 | } 46 | if (resources.getMem() <= 0.0) { 47 | errors.rejectValue("resources.mem", "resources.mem.not_positive", "mem must be a positive number"); 48 | } 49 | 50 | resources.getPorts().entrySet().stream() 51 | .forEach(entry -> { 52 | String name = "resources.ports." + entry.getKey(); 53 | if (isEmpty(entry.getValue().getHost())) { 54 | errors.rejectValue("resources.ports", name + ".host.empty"); 55 | } 56 | if (containerizedNetwork && entry.getValue().getContainer() < 1) { 57 | errors.rejectValue("resources.ports", name + ".container.empty"); 58 | } 59 | if (!containerizedNetwork && entry.getValue().getContainer() != 0) { 60 | errors.rejectValue("resources.ports", name + ".container.not_containerized"); 61 | } 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/CommandInfoMesosProtoFactory.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.config.MesosConfigProperties; 4 | import org.apache.mesos.Protos; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | 7 | import java.util.LinkedHashMap; 8 | import java.util.Map; 9 | import java.util.Optional; 10 | import java.util.stream.Collectors; 11 | 12 | public class CommandInfoMesosProtoFactory implements MesosProtoFactory> { 13 | private final MesosConfigProperties mesosConfig; 14 | 15 | public CommandInfoMesosProtoFactory(MesosConfigProperties mesosConfig) { 16 | this.mesosConfig = mesosConfig; 17 | } 18 | 19 | @Override 20 | public Protos.CommandInfo create(Map additionalEnvironmentVariables) { 21 | Protos.CommandInfo.Builder builder = Protos.CommandInfo.newBuilder(); 22 | Optional command = Optional.ofNullable(mesosConfig.getCommand()); 23 | builder.setShell(command.isPresent()); 24 | command.ifPresent(builder::setValue); 25 | builder.addAllUris(mesosConfig.getUri().stream().map(uri -> Protos.CommandInfo.URI.newBuilder().setValue(uri).build()).collect(Collectors.toList())); 26 | 27 | Map environmentVariables = new LinkedHashMap<>(); 28 | environmentVariables.putAll(additionalEnvironmentVariables); 29 | environmentVariables.putAll(mesosConfig.getEnvironment()); 30 | 31 | //todo: migrate to foreach 32 | environmentVariables.entrySet().stream() 33 | .map(kv -> Protos.Environment.Variable.newBuilder().setName(kv.getKey()).setValue(kv.getValue()).build()) 34 | .collect(Collectors.collectingAndThen( 35 | Collectors.toList(), 36 | variables -> builder.setEnvironment(Protos.Environment.newBuilder().addAllVariables(variables)))); 37 | return builder.build(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/CredentialFactory.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.config.MesosConfigProperties; 4 | import org.apache.mesos.Protos; 5 | 6 | /** 7 | * Creates credentials from user supplied principals and secrets 8 | */ 9 | public class CredentialFactory { 10 | private final MesosConfigProperties mesosConfig; 11 | 12 | public CredentialFactory(MesosConfigProperties mesosConfig) { 13 | this.mesosConfig = mesosConfig; 14 | } 15 | 16 | public Protos.Credential create() { 17 | if (mesosConfig.getPrincipal() != null && mesosConfig.getSecret() != null) { 18 | return Protos.Credential.newBuilder() 19 | .setPrincipal(mesosConfig.getPrincipal()) 20 | .setSecret(mesosConfig.getSecret()) 21 | .build(); 22 | } else { 23 | return Protos.Credential.getDefaultInstance(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/ExecutionParameters.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.requirements.PortMapping; 4 | import com.containersolutions.mesos.scheduler.requirements.VolumeMapping; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public class ExecutionParameters { 10 | Map environmentVariables; 11 | List portMappings; 12 | private List volumeMappings; 13 | 14 | public ExecutionParameters(Map environmentVariables, List portMappings, List volumeMappings) { 15 | this.environmentVariables = environmentVariables; 16 | this.portMappings = portMappings; 17 | this.volumeMappings = volumeMappings; 18 | } 19 | 20 | public Map getEnvironmentVariables() { 21 | return environmentVariables; 22 | } 23 | 24 | public List getPortMappings() { 25 | return portMappings; 26 | } 27 | 28 | public List getVolumeMappings() { 29 | return volumeMappings; 30 | } 31 | 32 | public void setVolumes(List volumes) { 33 | this.volumeMappings = volumes; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/FrameworkInfoFactory.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.config.MesosConfigProperties; 4 | import com.containersolutions.mesos.scheduler.state.StateRepository; 5 | import org.apache.commons.logging.Log; 6 | import org.apache.commons.logging.LogFactory; 7 | import org.apache.mesos.Protos; 8 | import org.springframework.beans.factory.annotation.Value; 9 | 10 | import java.util.Arrays; 11 | import java.util.Optional; 12 | 13 | /** 14 | * Creates framework info 15 | */ 16 | public class FrameworkInfoFactory { 17 | protected final Log logger = LogFactory.getLog(getClass()); 18 | 19 | @Value("${spring.application.name}") 20 | protected String applicationName; 21 | 22 | private final MesosConfigProperties mesosConfig; 23 | private final StateRepository stateRepository; 24 | private final CredentialFactory credentialFactory; 25 | 26 | public FrameworkInfoFactory(MesosConfigProperties mesosConfig, StateRepository stateRepository, CredentialFactory credentialFactory) { 27 | this.mesosConfig = mesosConfig; 28 | this.stateRepository = stateRepository; 29 | this.credentialFactory = credentialFactory; 30 | } 31 | 32 | public Protos.FrameworkInfo.Builder create() { 33 | Protos.FrameworkInfo.Builder frameworkBuilder = Protos.FrameworkInfo.newBuilder() 34 | .setName(applicationName) 35 | .setUser("root") 36 | .addRoles(mesosConfig.getRole()) 37 | .addCapabilities(Protos.FrameworkInfo.Capability.newBuilder().setType(Protos.FrameworkInfo.Capability.Type.MULTI_ROLE).build()) 38 | .setCheckpoint(true) 39 | .setFailoverTimeout(150.0) 40 | .setId(stateRepository.getFrameworkID().orElseGet(() -> Protos.FrameworkID.newBuilder().setValue("").build())); 41 | Protos.Credential credential = credentialFactory.create(); 42 | if (credential.isInitialized()) { 43 | logger.debug("Adding framework principal: " + credential.getPrincipal()); 44 | frameworkBuilder.setPrincipal(credential.getPrincipal()); 45 | } 46 | Optional.ofNullable(mesosConfig.getWebuiUrl()).ifPresent(frameworkBuilder::setWebuiUrl); 47 | return frameworkBuilder; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/InstanceCount.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.events.InstanceCountChangeEvent; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.ApplicationEventPublisher; 6 | import org.springframework.jmx.export.annotation.ManagedAttribute; 7 | import org.springframework.jmx.export.annotation.ManagedResource; 8 | 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | 11 | @ManagedResource 12 | public class InstanceCount { 13 | final AtomicInteger count; 14 | 15 | @Autowired 16 | ApplicationEventPublisher applicationEventPublisher; 17 | 18 | public InstanceCount(int count) { 19 | this.count = new AtomicInteger(count); 20 | } 21 | 22 | @ManagedAttribute 23 | public int getCount() { 24 | return count.get(); 25 | } 26 | 27 | @ManagedAttribute 28 | public void setCount(int count) { 29 | this.count.set(count); 30 | applicationEventPublisher.publishEvent(new InstanceCountChangeEvent(count)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/MesosProtoFactory.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | public interface MesosProtoFactory { 4 | T create(A argument); 5 | } 6 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/OfferStrategyFilter.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.requirements.OfferEvaluation; 4 | import com.containersolutions.mesos.scheduler.requirements.ResourceRequirement; 5 | import org.apache.commons.logging.Log; 6 | import org.apache.commons.logging.LogFactory; 7 | import org.apache.mesos.Protos; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.stream.Collectors; 13 | 14 | public class OfferStrategyFilter { 15 | protected final Log logger = LogFactory.getLog(getClass()); 16 | 17 | private final Map resourceRequirements; 18 | 19 | public OfferStrategyFilter(Map resourceRequirements) { 20 | this.resourceRequirements = resourceRequirements; 21 | } 22 | 23 | public OfferEvaluation evaluate(String taskId, Protos.Offer offer) { 24 | List offerEvaluations = resourceRequirements.entrySet().stream() 25 | .map(kv -> kv.getValue().check(kv.getKey(), taskId, offer)) 26 | .peek(offerEvaluation -> logger.debug((offerEvaluation.isValid() ? "Accepting" : "Rejecting") + " offer offerId=" + offer.getId().getValue() + ", by requirement=" + offerEvaluation.getRequirement() + offerEvaluation.getDeclineReason().map(reason -> " with reason=" + reason).orElse(""))) 27 | .collect(Collectors.toList()); 28 | 29 | if (offerEvaluations.stream().allMatch(OfferEvaluation::isValid)) { 30 | return OfferEvaluation.accept( 31 | "*", 32 | taskId, 33 | offer, 34 | offerEvaluations.stream().flatMap(offerEvaluation -> offerEvaluation.getEnvironmentVariables().entrySet().stream()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)), 35 | offerEvaluations.stream().flatMap(offerEvaluation -> offerEvaluation.getPortMappings().stream()).collect(Collectors.toList()), 36 | offerEvaluations.stream().flatMap(offerEvaluation -> offerEvaluation.getVolumeMappings().stream()).collect(Collectors.toList()), 37 | offerEvaluations.stream().flatMap(offerEvaluation -> offerEvaluation.getResources().stream()).collect(Collectors.toList()) 38 | ); 39 | } 40 | return OfferEvaluation.decline("*", taskId, offer, null); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/TaskInfoFactory.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public interface TaskInfoFactory { 9 | 10 | Protos.TaskInfo create(String taskId, Protos.Offer offer, List resources, ExecutionParameters executionParameters); 11 | } 12 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/TaskInfoFactoryCommand.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import org.apache.commons.logging.Log; 4 | import org.apache.commons.logging.LogFactory; 5 | import org.apache.mesos.Protos; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.UUID; 12 | import java.util.function.Supplier; 13 | 14 | public class TaskInfoFactoryCommand implements TaskInfoFactory { 15 | protected final Log logger = LogFactory.getLog(getClass()); 16 | 17 | @Value("${spring.application.name}") 18 | protected String applicationName; 19 | 20 | private final MesosProtoFactory> commandInfoMesosProtoFactory; 21 | 22 | private final Supplier uuidSupplier; 23 | 24 | public TaskInfoFactoryCommand(MesosProtoFactory> commandInfoMesosProtoFactory, Supplier uuidSupplier) { 25 | this.commandInfoMesosProtoFactory = commandInfoMesosProtoFactory; 26 | this.uuidSupplier = uuidSupplier; 27 | } 28 | 29 | @Override 30 | public Protos.TaskInfo create(String taskId, Protos.Offer offer, List resources, ExecutionParameters executionParameters) { 31 | logger.debug("Creating Mesos task for taskId=" + taskId); 32 | return Protos.TaskInfo.newBuilder() 33 | .setName(applicationName + ".task") 34 | .setSlaveId(offer.getSlaveId()) 35 | .setTaskId(Protos.TaskID.newBuilder().setValue(taskId)) 36 | .addAllResources(resources) 37 | .setCommand(commandInfoMesosProtoFactory.create(executionParameters.getEnvironmentVariables())) 38 | .build(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/TaskInfoFactoryDocker.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.config.MesosConfigProperties; 4 | import com.containersolutions.mesos.scheduler.requirements.PortMapping; 5 | import com.containersolutions.mesos.scheduler.requirements.VolumeMapping; 6 | import org.apache.commons.logging.Log; 7 | import org.apache.commons.logging.LogFactory; 8 | import org.apache.mesos.Protos; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Value; 11 | 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.stream.Collectors; 15 | 16 | import static java.util.Collections.emptyList; 17 | 18 | public class TaskInfoFactoryDocker implements TaskInfoFactory { 19 | protected final Log logger = LogFactory.getLog(getClass()); 20 | 21 | @Value("${mesos.docker.image}") 22 | protected String dockerImage; 23 | 24 | @Value("${spring.application.name}") 25 | protected String applicationName; 26 | 27 | @Value("${mesos.docker.network:BRIDGE}") 28 | protected String networkMode; // May be BRIDGE or HOST 29 | 30 | private final MesosConfigProperties mesosConfig; 31 | 32 | private final MesosProtoFactory> commandInfoMesosProtoFactory; 33 | 34 | public TaskInfoFactoryDocker(MesosConfigProperties mesosConfig, MesosProtoFactory> commandInfoMesosProtoFactory) { 35 | this.mesosConfig = mesosConfig; 36 | this.commandInfoMesosProtoFactory = commandInfoMesosProtoFactory; 37 | } 38 | 39 | @Override 40 | public Protos.TaskInfo create(String taskId, Protos.Offer offer, List resources, ExecutionParameters executionParameters) { 41 | logger.info("Creating task with taskId=" + taskId + " from offerId=" + offer.getId().getValue()); 42 | return Protos.TaskInfo.newBuilder() 43 | .setName(applicationName + ".task") 44 | .setSlaveId(offer.getSlaveId()) 45 | .setTaskId(Protos.TaskID.newBuilder().setValue(taskId)) 46 | .addAllResources(resources) 47 | .setContainer(Protos.ContainerInfo.newBuilder() 48 | .setType(Protos.ContainerInfo.Type.DOCKER) 49 | .setDocker(Protos.ContainerInfo.DockerInfo.newBuilder() 50 | .setImage(dockerImage) 51 | .addAllPortMappings(portMappings(executionParameters.getPortMappings())) 52 | .setNetwork(Protos.ContainerInfo.DockerInfo.Network.valueOf(networkMode)) 53 | ) 54 | .addAllVolumes(volumeMappings(executionParameters.getVolumeMappings())) 55 | ) 56 | .setCommand(command(executionParameters.getEnvironmentVariables())) 57 | .build(); 58 | } 59 | 60 | private Iterable volumeMappings(List volumeMappings) { 61 | return volumeMappings.stream() 62 | .map(volumeMapping -> Protos.Volume.newBuilder() 63 | .setHostPath(volumeMapping.getHostPath()) 64 | .setContainerPath(volumeMapping.getContainerPath()) 65 | .setMode(volumeMapping.isReadOnly() ? Protos.Volume.Mode.RO : Protos.Volume.Mode.RW) 66 | .build()) 67 | .peek(volume -> logger.info("Mapping host volume to container volume")) 68 | .collect(Collectors.toList()); 69 | } 70 | 71 | private Protos.CommandInfo command(Map additionalEnvironmentVariables) { 72 | return commandInfoMesosProtoFactory.create(additionalEnvironmentVariables); 73 | } 74 | 75 | private Iterable portMappings(List portMappings) { 76 | if (networkMode.equalsIgnoreCase("BRIDGE")) { 77 | return portMappings.stream() 78 | .map(portMapping -> Protos.ContainerInfo.DockerInfo.PortMapping.newBuilder() 79 | .setHostPort(portMapping.getOfferedPort()) 80 | .setContainerPort(portMapping.getContainerPort().orElseThrow(() -> new IllegalArgumentException("No container port specified for " + portMapping.getName()))) 81 | .build()) 82 | .peek(portMapping -> logger.info("Mapped host port " + portMapping.getHostPort() + " to container port " + portMapping.getContainerPort())) 83 | .collect(Collectors.toList()); 84 | } else { 85 | return emptyList(); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/TaskMaterializer.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.requirements.OfferEvaluation; 4 | 5 | public interface TaskMaterializer { 6 | TaskProposal createProposal(OfferEvaluation offerEvaluation); 7 | } 8 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/TaskMaterializerMinimal.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.requirements.OfferEvaluation; 4 | import org.apache.mesos.Protos; 5 | 6 | import java.util.stream.Collectors; 7 | 8 | public class TaskMaterializerMinimal implements TaskMaterializer { 9 | private final TaskInfoFactory taskInfoFactory; 10 | 11 | public TaskMaterializerMinimal(TaskInfoFactory taskInfoFactory) { 12 | this.taskInfoFactory = taskInfoFactory; 13 | } 14 | 15 | @Override 16 | public TaskProposal createProposal(OfferEvaluation offerEvaluation) { 17 | return new TaskProposal( 18 | offerEvaluation.getOffer(), 19 | taskInfoFactory.create( 20 | offerEvaluation.getTaskId(), 21 | offerEvaluation.getOffer(), 22 | offerEvaluation.getResources().stream().filter(resource -> resource.getType() != Protos.Value.Type.RANGES || !resource.getRanges().getRangeList().isEmpty()).collect(Collectors.toList()), 23 | new ExecutionParameters( 24 | offerEvaluation.getEnvironmentVariables(), 25 | offerEvaluation.getPortMappings(), 26 | offerEvaluation.getVolumeMappings() 27 | ) 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/TaskProposal.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | class TaskProposal { 6 | Protos.Offer offer; 7 | Protos.TaskInfo taskInfo; 8 | 9 | public TaskProposal(Protos.Offer offer, Protos.TaskInfo taskInfo) { 10 | this.offer = offer; 11 | this.taskInfo = taskInfo; 12 | } 13 | 14 | public Protos.OfferID getOfferId() { 15 | return offer.getId(); 16 | } 17 | 18 | public Protos.TaskInfo getTaskInfo() { 19 | return taskInfo; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/TaskReaper.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.events.InstanceCountChangeEvent; 4 | import com.containersolutions.mesos.scheduler.state.StateRepository; 5 | import org.apache.commons.logging.Log; 6 | import org.apache.commons.logging.LogFactory; 7 | import org.apache.mesos.Protos; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.ApplicationListener; 10 | 11 | import java.util.Set; 12 | 13 | public class TaskReaper implements ApplicationListener { 14 | private final Log logger = LogFactory.getLog(getClass()); 15 | 16 | private final StateRepository stateRepository; 17 | private final InstanceCount instanceCount; 18 | private final UniversalScheduler universalScheduler; 19 | 20 | public TaskReaper(StateRepository stateRepository, InstanceCount instanceCount, UniversalScheduler universalScheduler) { 21 | this.stateRepository = stateRepository; 22 | this.instanceCount = instanceCount; 23 | this.universalScheduler = universalScheduler; 24 | } 25 | 26 | @Override 27 | public void onApplicationEvent(InstanceCountChangeEvent event) { 28 | int expectedInstances = instanceCount.getCount(); 29 | Set taskInfos = stateRepository.allTaskInfos(); 30 | int runningInstances = taskInfos.size(); 31 | 32 | int rest = runningInstances - expectedInstances; 33 | taskInfos.stream() 34 | .limit(Math.max(0, rest)) 35 | .map(Protos.TaskInfo::getTaskId) 36 | .peek(taskId -> logger.info("Killing taskId=" + taskId.getValue())) 37 | .forEach(universalScheduler::killTask); 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/TeardownManager.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.events.FrameworkRemovedEvent; 4 | import com.containersolutions.mesos.scheduler.events.TearDownFrameworkEvent; 5 | import org.apache.commons.logging.Log; 6 | import org.apache.commons.logging.LogFactory; 7 | import org.springframework.beans.BeansException; 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.context.ApplicationContext; 10 | import org.springframework.context.ApplicationContextAware; 11 | import org.springframework.context.ApplicationEventPublisher; 12 | import org.springframework.context.ApplicationEventPublisherAware; 13 | import org.springframework.context.event.EventListener; 14 | 15 | public class TeardownManager implements ApplicationContextAware { 16 | protected final Log logger = LogFactory.getLog(getClass()); 17 | private ApplicationContext applicationContext; 18 | 19 | @EventListener 20 | public void onFrameworkRemovedEvent(FrameworkRemovedEvent event) { 21 | logger.info("Framework removed. Shutting down scheduler, reason=\"" + event.getSource() + "\""); 22 | System.exit(SpringApplication.exit(applicationContext, () -> 10)); 23 | } 24 | @EventListener 25 | public void onTearDownFrameworkEvent(TearDownFrameworkEvent event) { 26 | logger.info("Framework shutting down"); 27 | final Thread shutdownTask = new Thread(() -> { 28 | try { 29 | Thread.sleep(2000); 30 | } catch (InterruptedException e) { 31 | e.printStackTrace(); 32 | } 33 | System.exit(SpringApplication.exit(applicationContext, () -> 0)); 34 | }); 35 | shutdownTask.setDaemon(true); 36 | shutdownTask.start(); 37 | } 38 | 39 | public void teardownFramework() { 40 | System.exit(SpringApplication.exit(applicationContext, () -> 10)); 41 | } 42 | 43 | @Override 44 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 45 | this.applicationContext = applicationContext; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/UniversalScheduler.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.config.MesosConfigProperties; 4 | import com.containersolutions.mesos.scheduler.events.*; 5 | import com.containersolutions.mesos.scheduler.requirements.OfferEvaluation; 6 | import com.containersolutions.mesos.scheduler.state.StateRepository; 7 | import com.containersolutions.mesos.utils.StreamHelper; 8 | import org.apache.commons.logging.Log; 9 | import org.apache.commons.logging.LogFactory; 10 | import org.apache.mesos.MesosSchedulerDriver; 11 | import org.apache.mesos.Protos; 12 | import org.apache.mesos.Scheduler; 13 | import org.apache.mesos.SchedulerDriver; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.boot.context.event.ApplicationReadyEvent; 16 | import org.springframework.context.ApplicationEventPublisher; 17 | import org.springframework.context.ApplicationListener; 18 | import org.springframework.context.event.EventListener; 19 | 20 | import javax.annotation.PreDestroy; 21 | import java.util.Collections; 22 | import java.util.List; 23 | import java.util.UUID; 24 | import java.util.concurrent.ExecutionException; 25 | import java.util.concurrent.atomic.AtomicInteger; 26 | import java.util.concurrent.atomic.AtomicReference; 27 | import java.util.function.Supplier; 28 | 29 | public class UniversalScheduler implements Scheduler, ApplicationListener { 30 | protected final Log logger = LogFactory.getLog(getClass()); 31 | 32 | @Value("${mesos.master}") 33 | protected String mesosMaster; 34 | 35 | @Value("${mesos.zookeeper.server}") 36 | protected String zookeeperMaster; 37 | 38 | @Value("${spring.application.name}") 39 | protected String applicationName; 40 | 41 | private final MesosConfigProperties mesosConfig; 42 | 43 | private final OfferStrategyFilter offerStrategyFilter; 44 | 45 | private final ApplicationEventPublisher applicationEventPublisher; 46 | 47 | private final Supplier uuidSupplier; 48 | 49 | private final StateRepository stateRepository; 50 | 51 | private final TaskMaterializer taskMaterializer; 52 | 53 | private final FrameworkInfoFactory frameworkInfoFactory; 54 | 55 | private final CredentialFactory credentialFactory; 56 | 57 | protected AtomicReference frameworkID = new AtomicReference<>(); 58 | 59 | protected AtomicReference driver = new AtomicReference<>(); 60 | 61 | public UniversalScheduler(MesosConfigProperties mesosConfig, OfferStrategyFilter offerStrategyFilter, ApplicationEventPublisher applicationEventPublisher, Supplier uuidSupplier, StateRepository stateRepository, TaskMaterializer taskMaterializer, FrameworkInfoFactory frameworkInfoFactory, CredentialFactory credentialFactory) { 62 | this.mesosConfig = mesosConfig; 63 | this.offerStrategyFilter = offerStrategyFilter; 64 | this.applicationEventPublisher = applicationEventPublisher; 65 | this.uuidSupplier = uuidSupplier; 66 | this.stateRepository = stateRepository; 67 | this.taskMaterializer = taskMaterializer; 68 | this.frameworkInfoFactory = frameworkInfoFactory; 69 | this.credentialFactory = credentialFactory; 70 | } 71 | 72 | @Override 73 | public void onApplicationEvent(ApplicationReadyEvent event) { 74 | start(); 75 | } 76 | 77 | public void start() { 78 | logger.info("Starting Mesos-Starter Framework"); 79 | 80 | MesosSchedulerDriver driver; 81 | Protos.Credential credential = credentialFactory.create(); 82 | final String zookeeperUrl = "zk://" + zookeeperMaster + "/mesos"; 83 | if (credential.isInitialized()) { 84 | logger.debug("Starting scheduler driver with supplied credentials for principal=" + credential.getPrincipal()); 85 | driver = new MesosSchedulerDriver(this, frameworkInfoFactory.create().build(), zookeeperUrl, credential); 86 | } else { 87 | logger.debug("Starting scheduler driver without authorisation."); 88 | //TODO: The follow MesosSchedulerDriver constructor will be deprecated at some point. 89 | driver = new MesosSchedulerDriver(this, frameworkInfoFactory.create().build(), zookeeperUrl); 90 | } 91 | 92 | if (!this.driver.compareAndSet(null, driver)) { 93 | throw new IllegalStateException("Driver already initialised"); 94 | } 95 | driver.start(); 96 | 97 | new Thread(driver::join).start(); 98 | } 99 | 100 | @PreDestroy 101 | public void stop() { 102 | if (driver.get() != null) { 103 | driver.get().abort(); 104 | logger.info("Driver aborted"); 105 | } 106 | } 107 | 108 | 109 | @Override 110 | public void registered(SchedulerDriver schedulerDriver, Protos.FrameworkID frameworkID, Protos.MasterInfo masterInfo) { 111 | logger.info("Framework registrered with frameworkId=" + frameworkID.getValue()); 112 | this.frameworkID.set(frameworkID); 113 | applicationEventPublisher.publishEvent(new FrameworkRegistreredEvent(frameworkID, masterInfo)); 114 | } 115 | 116 | @Override 117 | public void reregistered(SchedulerDriver schedulerDriver, Protos.MasterInfo masterInfo) { 118 | logger.info("Framework re-registrered"); 119 | applicationEventPublisher.publishEvent(new FrameworkReregistreredEvent(masterInfo)); 120 | } 121 | 122 | @Override 123 | public void resourceOffers(SchedulerDriver schedulerDriver, List offers) { 124 | logger.info("Initiating new offer round of " + offers.size() + " offers"); 125 | AtomicInteger acceptedOffers = new AtomicInteger(0); 126 | AtomicInteger rejectedOffers = new AtomicInteger(0); 127 | offers.stream() 128 | .peek(offer -> logger.debug("Received offerId=" + offer.getId().getValue() + " for slaveId=" + offer.getSlaveId().getValue())) 129 | .map(offer -> offerStrategyFilter.evaluate(uuidSupplier.get().toString(), offer)) 130 | .peek(offerEvaluation -> (offerEvaluation.isValid() ? acceptedOffers : rejectedOffers).incrementAndGet()) 131 | .filter(StreamHelper.onNegative( 132 | OfferEvaluation::isValid, 133 | offerEvaluation -> schedulerDriver.declineOffer(offerEvaluation.getOffer().getId()))) 134 | .peek(offerEvaluation -> logger.info("Accepting offer offerId=" + offerEvaluation.getOffer().getId().getValue() + " on slaveId=" + offerEvaluation.getOffer().getSlaveId().getValue())) 135 | .map(taskMaterializer::createProposal) 136 | // .peek(taskProposal -> logger.debug("Launching task " + taskProposal.getTaskInfo().toString())) 137 | .forEach(taskProposal -> { 138 | schedulerDriver.launchTasks(Collections.singleton(taskProposal.getOfferId()), Collections.singleton(taskProposal.getTaskInfo())); 139 | stateRepository.store(taskProposal.taskInfo); 140 | }); 141 | logger.info("Finished evaluating " + offers.size() + " offers. Accepted " + acceptedOffers.get() + " offers and rejected " + rejectedOffers.get()); 142 | } 143 | 144 | @Override 145 | public void offerRescinded(SchedulerDriver schedulerDriver, Protos.OfferID offerID) { 146 | logger.warn("Offer rescinded offerId=" + offerID.getValue()); 147 | } 148 | 149 | @Override 150 | public void statusUpdate(SchedulerDriver schedulerDriver, Protos.TaskStatus taskStatus) { 151 | if (taskStatus.getState().equals(Protos.TaskState.TASK_ERROR)) { 152 | logger.warn("Received status update for taskID=" + taskStatus.getTaskId().getValue() + " state=" + taskStatus.getState() + " message='" + taskStatus.getMessage() + "' "); 153 | } 154 | 155 | applicationEventPublisher.publishEvent(new StatusUpdateEvent(taskStatus)); 156 | } 157 | 158 | @Override 159 | public void frameworkMessage(SchedulerDriver schedulerDriver, Protos.ExecutorID executorID, Protos.SlaveID slaveID, byte[] data) { 160 | logger.debug("Received framework message from slaveId=" + slaveID.getValue()); 161 | applicationEventPublisher.publishEvent(new FrameworkMessageEvent(data, executorID, slaveID)); 162 | } 163 | 164 | @Override 165 | public void disconnected(SchedulerDriver schedulerDriver) { 166 | logger.warn("Disconnected"); 167 | } 168 | 169 | @Override 170 | public void slaveLost(SchedulerDriver schedulerDriver, Protos.SlaveID slaveID) { 171 | logger.info("Received slave lost on slaveId=" + slaveID.getValue()); 172 | applicationEventPublisher.publishEvent(new SlaveLostEvent(slaveID)); 173 | } 174 | 175 | @Override 176 | public void executorLost(SchedulerDriver schedulerDriver, Protos.ExecutorID executorID, Protos.SlaveID slaveID, int status) { 177 | logger.info("Received executor lost on slaveId=" + slaveID.getValue()); 178 | applicationEventPublisher.publishEvent(new ExecutorLostEvent(status, executorID, slaveID)); 179 | } 180 | 181 | @Override 182 | public void error(SchedulerDriver schedulerDriver, String message) { 183 | logger.error("Received error: " + message); 184 | 185 | if (message.equalsIgnoreCase("Framework has been removed")) { 186 | applicationEventPublisher.publishEvent(new FrameworkRemovedEvent(message)); 187 | } 188 | applicationEventPublisher.publishEvent(new ErrorEvent(message)); 189 | } 190 | 191 | public void killTask(Protos.TaskID taskId) { 192 | driver.get().killTask(taskId); 193 | } 194 | 195 | public void sendFrameworkMessage(String executorId, String slaveId, byte[] data) { 196 | driver.get().sendFrameworkMessage(Protos.ExecutorID.newBuilder().setValue(executorId).build(), Protos.SlaveID.newBuilder().setValue(slaveId).build(), data); 197 | } 198 | 199 | @EventListener 200 | public void onTearDownFrameworkEvent(TearDownFrameworkEvent event) { 201 | driver.get().stop(); 202 | logger.info("Driver stopped"); 203 | driver.set(null); 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/config/DockerConfigProperties.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.config; 2 | 3 | public class DockerConfigProperties { 4 | private String image; 5 | private String network; 6 | 7 | public String getImage() { 8 | return image; 9 | } 10 | 11 | public void setImage(String image) { 12 | this.image = image; 13 | } 14 | 15 | public String getNetwork() { 16 | return network; 17 | } 18 | 19 | public void setNetwork(String network) { 20 | this.network = network; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/config/MesosConfigProperties.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | @ConfigurationProperties(prefix = "mesos") 11 | public class MesosConfigProperties { 12 | private String master; 13 | private ZookeeperConfigProperties zookeeper; 14 | private String role = "*"; 15 | private String command; 16 | private DockerConfigProperties docker; 17 | private Map environment = new HashMap<>(); 18 | private ResourcesConfigProperties resources = new ResourcesConfigProperties(); 19 | private String principal; 20 | private String secret; 21 | private List uri = new ArrayList<>(); 22 | private String webuiUrl; 23 | 24 | public String getMaster() { 25 | return master; 26 | } 27 | 28 | public void setMaster(String master) { 29 | this.master = master; 30 | } 31 | 32 | public ZookeeperConfigProperties getZookeeper() { 33 | return zookeeper; 34 | } 35 | 36 | public void setZookeeper(ZookeeperConfigProperties zookeeper) { 37 | this.zookeeper = zookeeper; 38 | } 39 | 40 | public String getRole() { 41 | return role; 42 | } 43 | 44 | public void setRole(String role) { 45 | this.role = role; 46 | } 47 | 48 | public String getCommand() { 49 | return command; 50 | } 51 | 52 | public void setCommand(String command) { 53 | this.command = command; 54 | } 55 | 56 | public Map getEnvironment() { 57 | return environment; 58 | } 59 | 60 | public void setEnvironment(Map environment) { 61 | this.environment = environment; 62 | } 63 | 64 | public ResourcesConfigProperties getResources() { 65 | return resources; 66 | } 67 | 68 | public void setResources(ResourcesConfigProperties resources) { 69 | this.resources = resources; 70 | } 71 | 72 | public String getPrincipal() { 73 | return principal; 74 | } 75 | 76 | public void setPrincipal(String principal) { 77 | this.principal = principal; 78 | } 79 | 80 | public String getSecret() { 81 | return secret; 82 | } 83 | 84 | public void setSecret(String secret) { 85 | this.secret = secret; 86 | } 87 | 88 | public List getUri() { 89 | return uri; 90 | } 91 | 92 | public void setUri(List uri) { 93 | this.uri = uri; 94 | } 95 | 96 | public DockerConfigProperties getDocker() { 97 | return docker; 98 | } 99 | 100 | public void setDocker(DockerConfigProperties docker) { 101 | this.docker = docker; 102 | } 103 | 104 | public String getWebuiUrl() { 105 | return webuiUrl; 106 | } 107 | 108 | public void setWebuiUrl(String webuiUrl) { 109 | this.webuiUrl = webuiUrl; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/config/ResourcePortConfigProperties.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.config; 2 | 3 | public class ResourcePortConfigProperties { 4 | String host; 5 | int container = 0; 6 | String role; 7 | 8 | public String getHost() { 9 | return host; 10 | } 11 | 12 | public void setHost(String host) { 13 | this.host = host; 14 | } 15 | 16 | public int getContainer() { 17 | return container; 18 | } 19 | 20 | public void setContainer(int container) { 21 | this.container = container; 22 | } 23 | 24 | public String getRole() { 25 | return role; 26 | } 27 | 28 | public void setRole(String role) { 29 | this.role = role; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/config/ResourceVolumeConfigProperties.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.config; 2 | 3 | public class ResourceVolumeConfigProperties { 4 | String hostPath; 5 | String containerPath; 6 | boolean readonly = false; 7 | 8 | public String getHostPath() { 9 | return hostPath; 10 | } 11 | 12 | public void setHostPath(String hostPath) { 13 | this.hostPath = hostPath; 14 | } 15 | 16 | public String getContainerPath() { 17 | return containerPath; 18 | } 19 | 20 | public void setContainerPath(String containerPath) { 21 | this.containerPath = containerPath; 22 | } 23 | 24 | public boolean isReadonly() { 25 | return readonly; 26 | } 27 | 28 | public void setReadonly(boolean readonly) { 29 | this.readonly = readonly; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/config/ResourcesConfigProperties.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.config; 2 | 3 | import java.util.ArrayList; 4 | import java.util.LinkedHashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class ResourcesConfigProperties { 9 | 10 | private double cpus; 11 | private double mem; 12 | 13 | private Map ports = new LinkedHashMap<>(); 14 | private List volumes = new ArrayList<>(); 15 | private int count; 16 | 17 | public double getCpus() { 18 | return cpus; 19 | } 20 | 21 | public void setCpus(double cpus) { 22 | this.cpus = cpus; 23 | } 24 | 25 | public double getMem() { 26 | return mem; 27 | } 28 | 29 | public void setMem(double mem) { 30 | this.mem = mem; 31 | } 32 | 33 | public Map getPorts() { 34 | return ports; 35 | } 36 | 37 | public void setPorts(Map ports) { 38 | this.ports = ports; 39 | } 40 | 41 | public List getVolumes() { 42 | return volumes; 43 | } 44 | 45 | public void setVolumes(List volumes) { 46 | this.volumes = volumes; 47 | } 48 | 49 | public void setCount(int count) { 50 | this.count = count; 51 | } 52 | 53 | public int getCount() { 54 | return count; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/config/ZookeeperConfigProperties.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.config; 2 | 3 | public class ZookeeperConfigProperties { 4 | private String server; 5 | 6 | public String getServer() { 7 | return server; 8 | } 9 | 10 | public void setServer(String server) { 11 | this.server = server; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/events/ErrorEvent.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.events; 2 | 3 | public class ErrorEvent extends MesosEvent { 4 | public ErrorEvent(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/events/ExecutorLostEvent.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.events; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | public class ExecutorLostEvent extends MesosEvent { 6 | private final Protos.ExecutorID executorID; 7 | private final Protos.SlaveID slaveID; 8 | 9 | public ExecutorLostEvent(int status, Protos.ExecutorID executorID, Protos.SlaveID slaveID) { 10 | super(status); 11 | this.executorID = executorID; 12 | this.slaveID = slaveID; 13 | } 14 | 15 | public Protos.ExecutorID getExecutorID() { 16 | return executorID; 17 | } 18 | 19 | public Protos.SlaveID getSlaveID() { 20 | return slaveID; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/events/FrameworkMessageEvent.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.events; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | public class FrameworkMessageEvent extends MesosEvent { 6 | private final Protos.ExecutorID executorID; 7 | private final Protos.SlaveID slaveID; 8 | 9 | /** 10 | * Create a new ApplicationEvent. 11 | * 12 | * @param data the component that published the event (never {@code null}) 13 | * @param executorID 14 | * @param slaveID 15 | */ 16 | public FrameworkMessageEvent(byte[] data, Protos.ExecutorID executorID, Protos.SlaveID slaveID) { 17 | super(data); 18 | this.executorID = executorID; 19 | this.slaveID = slaveID; 20 | } 21 | 22 | public Protos.ExecutorID getExecutorID() { 23 | return executorID; 24 | } 25 | 26 | public Protos.SlaveID getSlaveID() { 27 | return slaveID; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/events/FrameworkRegistreredEvent.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.events; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | public class FrameworkRegistreredEvent extends MesosEvent { 6 | private final Protos.MasterInfo masterInfo; 7 | 8 | public FrameworkRegistreredEvent(Protos.FrameworkID frameworkID, Protos.MasterInfo masterInfo) { 9 | super(frameworkID); 10 | this.masterInfo = masterInfo; 11 | } 12 | 13 | public Protos.FrameworkID getFrameworkID() { 14 | return (Protos.FrameworkID) getSource(); 15 | } 16 | 17 | public Protos.MasterInfo getMasterInfo() { 18 | return masterInfo; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/events/FrameworkRemovedEvent.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.events; 2 | 3 | /** 4 | * Remote party has removed the framework. 5 | */ 6 | public class FrameworkRemovedEvent extends MesosEvent { 7 | public FrameworkRemovedEvent(String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/events/FrameworkReregistreredEvent.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.events; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | public class FrameworkReregistreredEvent extends MesosEvent { 6 | public FrameworkReregistreredEvent(Protos.MasterInfo masterInfo) { 7 | super(masterInfo); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/events/InstanceCountChangeEvent.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.events; 2 | 3 | import org.springframework.context.ApplicationEvent; 4 | 5 | public class InstanceCountChangeEvent extends ApplicationEvent { 6 | public InstanceCountChangeEvent(int newCount) { 7 | super(newCount); 8 | } 9 | 10 | public int getCount() { 11 | return (int) getSource(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/events/MesosEvent.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.events; 2 | 3 | import org.springframework.context.ApplicationEvent; 4 | 5 | public abstract class MesosEvent extends ApplicationEvent { 6 | /** 7 | * Create a new ApplicationEvent. 8 | * 9 | * @param source the component that published the event (never {@code null}) 10 | */ 11 | public MesosEvent(Object source) { 12 | super(source); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/events/SlaveLostEvent.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.events; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | public class SlaveLostEvent extends MesosEvent { 6 | public SlaveLostEvent(Protos.SlaveID slaveID) { 7 | super(slaveID); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/events/StatusUpdateEvent.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.events; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | public class StatusUpdateEvent extends MesosEvent { 6 | public StatusUpdateEvent(Protos.TaskStatus taskStatus) { 7 | super(taskStatus); 8 | } 9 | 10 | public Protos.TaskStatus getTaskStatus() { 11 | return (Protos.TaskStatus) getSource(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/events/TearDownFrameworkEvent.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.events; 2 | 3 | public class TearDownFrameworkEvent extends MesosEvent { 4 | /** 5 | * Create a new ApplicationEvent. 6 | * 7 | * @param source the component that published the event (never {@code null}) 8 | */ 9 | public TearDownFrameworkEvent(Object source) { 10 | super(source); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/requirements/DistinctSlaveRequirement.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.requirements; 2 | 3 | import com.containersolutions.mesos.scheduler.events.StatusUpdateEvent; 4 | import com.containersolutions.mesos.scheduler.state.StateRepository; 5 | import org.apache.commons.logging.Log; 6 | import org.apache.commons.logging.LogFactory; 7 | import org.apache.mesos.Protos; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.event.EventListener; 10 | 11 | import java.time.Clock; 12 | import java.time.Instant; 13 | import java.util.Collections; 14 | import java.util.Map; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | 17 | import static com.containersolutions.mesos.utils.MesosHelper.isTerminalTaskState; 18 | 19 | public class DistinctSlaveRequirement implements ResourceRequirement { 20 | protected final Log logger = LogFactory.getLog(getClass()); 21 | 22 | private final Clock clock; 23 | 24 | private final StateRepository stateRepository; 25 | 26 | private Map tentativeAccept = new ConcurrentHashMap<>(); 27 | 28 | public DistinctSlaveRequirement(Clock clock, StateRepository stateRepository) { 29 | this.clock = clock; 30 | this.stateRepository = stateRepository; 31 | } 32 | 33 | @Override 34 | public OfferEvaluation check(String requirement, String taskId, Protos.Offer offer) { 35 | final Instant now = clock.instant(); 36 | cleanUpTentatives(now); 37 | 38 | final String slaveId = offer.getSlaveId().getValue(); 39 | final boolean valid = slaveIsRunningTask(slaveId); 40 | if (valid) { 41 | tentativeAccept.put(slaveId, now.plusSeconds(60)); 42 | return OfferEvaluation.accept(requirement, taskId, offer, Collections.emptyMap(), Collections.emptyList(), Collections.emptyList()); 43 | } 44 | return OfferEvaluation.decline(requirement, taskId, offer, "Slave " + slaveId + " is already running task"); 45 | } 46 | 47 | private boolean slaveIsRunningTask(String slaveId) { 48 | return stateRepository.allTaskInfos().stream().noneMatch(taskInfo -> taskInfo.getSlaveId().getValue().equals(slaveId)) && !tentativeAccept.containsKey(slaveId); 49 | } 50 | 51 | private void cleanUpTentatives(Instant now) { 52 | tentativeAccept.entrySet().stream() 53 | .filter(entry -> entry.getValue().isBefore(now)) 54 | .map(Map.Entry::getKey) 55 | .peek(key -> logger.debug("removing key = " + key)) 56 | .forEach(tentativeAccept::remove); 57 | } 58 | 59 | @EventListener 60 | public void onStatusUpdate(StatusUpdateEvent event) { 61 | if (isTerminalTaskState(event.getTaskStatus().getState())) { 62 | if (tentativeAccept.remove(event.getTaskStatus().getSlaveId().getValue()) != null && logger.isDebugEnabled()) { 63 | logger.debug("Removed tentative accept for SlaveId=" + event.getTaskStatus().getSlaveId().getValue()); 64 | } 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/requirements/InstancesCountRequirement.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.requirements; 2 | 3 | import com.containersolutions.mesos.scheduler.InstanceCount; 4 | import com.containersolutions.mesos.scheduler.state.StateRepository; 5 | import org.apache.commons.logging.Log; 6 | import org.apache.commons.logging.LogFactory; 7 | import org.apache.mesos.Protos; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | 10 | import java.util.Collections; 11 | 12 | public class InstancesCountRequirement implements ResourceRequirement { 13 | private final Log logger = LogFactory.getLog(getClass()); 14 | 15 | private final StateRepository stateRepository; 16 | 17 | private final InstanceCount instanceCount; 18 | 19 | public InstancesCountRequirement(StateRepository stateRepository, InstanceCount instanceCount) { 20 | this.stateRepository = stateRepository; 21 | this.instanceCount = instanceCount; 22 | } 23 | 24 | @Override 25 | public OfferEvaluation check(String requirement, String taskId, Protos.Offer offer) { 26 | int totalCount = tasksCount(); 27 | int instanceCount = this.instanceCount.getCount(); 28 | 29 | if (totalCount < instanceCount) { 30 | return OfferEvaluation.accept(requirement, taskId, offer, Collections.emptyMap(), Collections.emptyList(), Collections.emptyList()); 31 | } 32 | return OfferEvaluation.decline(requirement, taskId, offer, "tasksCount=" + totalCount + " < instanceCount=" + instanceCount); 33 | } 34 | 35 | private int tasksCount() { 36 | return stateRepository.allTaskInfos().size(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/requirements/OfferEvaluation.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.requirements; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Optional; 8 | 9 | import static java.util.Arrays.asList; 10 | 11 | public class OfferEvaluation { 12 | String requirement; 13 | Protos.Offer offer; 14 | boolean valid; 15 | String declineReason; 16 | String taskId; 17 | private Map environmentVariables; 18 | final List portMappings; 19 | List resources; 20 | private List volumeMappings; 21 | 22 | private OfferEvaluation(String requirement, String taskId, Protos.Offer offer, boolean valid, String declineReason, Map environmentVariables, List portMappings, List volumeMappings, List resources) { 23 | this.requirement = requirement; 24 | this.taskId = taskId; 25 | this.offer = offer; 26 | this.valid = valid; 27 | this.declineReason = declineReason; 28 | this.environmentVariables = environmentVariables; 29 | this.portMappings = portMappings; 30 | this.volumeMappings = volumeMappings; 31 | this.resources = resources; 32 | } 33 | 34 | public static OfferEvaluation accept(String requirement, String taskId, Protos.Offer offer, Map environmentVariables, List portMappings, List volumeMappings, List resources) { 35 | return new OfferEvaluation(requirement, taskId, offer, true, null, environmentVariables, portMappings, volumeMappings, resources); 36 | } 37 | 38 | public static OfferEvaluation accept(String requirement, String taskId, Protos.Offer offer, Map environmentVariables, List portMappings, List volumeMappings, Protos.Resource ... resources) { 39 | return new OfferEvaluation(requirement, taskId, offer, true, null, environmentVariables, portMappings, volumeMappings, asList(resources)); 40 | } 41 | 42 | public static OfferEvaluation decline(String requirement, String taskId, Protos.Offer offer, String reason) { 43 | return new OfferEvaluation(requirement, taskId, offer, false, reason, null, null, null, null); 44 | } 45 | 46 | public boolean isValid() { 47 | return valid; 48 | } 49 | 50 | public Protos.Offer getOffer() { 51 | return offer; 52 | } 53 | 54 | public List getResources() { 55 | return resources; 56 | } 57 | 58 | public String getTaskId() { 59 | return taskId; 60 | } 61 | 62 | public String getRequirement() { 63 | return requirement; 64 | } 65 | 66 | public Map getEnvironmentVariables() { 67 | return environmentVariables; 68 | } 69 | 70 | public void setEnvironmentVariables(Map environmentVariables) { 71 | this.environmentVariables = environmentVariables; 72 | } 73 | 74 | public List getPortMappings() { 75 | return portMappings; 76 | } 77 | 78 | public Optional getDeclineReason() { 79 | return Optional.ofNullable(this.declineReason); 80 | } 81 | 82 | public List getVolumeMappings() { 83 | return volumeMappings; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/requirements/PortMapping.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.requirements; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | import java.util.OptionalInt; 6 | 7 | public class PortMapping { 8 | String name; 9 | int offeredPort; 10 | int containerPort = 0; 11 | 12 | public PortMapping(String name, int offeredPort, int containerPort) { 13 | this.name = name; 14 | this.offeredPort = offeredPort; 15 | this.containerPort = containerPort; 16 | } 17 | 18 | public String envName() { 19 | return name.toUpperCase().replace("-", "_"); 20 | } 21 | 22 | public String envValue() { 23 | return String.valueOf(offeredPort); 24 | } 25 | 26 | public int getOfferedPort() { 27 | return offeredPort; 28 | } 29 | 30 | public OptionalInt getContainerPort() { 31 | return OptionalInt.of(containerPort); 32 | } 33 | 34 | Protos.Value.Range toRange() { 35 | return Protos.Value.Range.newBuilder().setBegin(offeredPort).setEnd(offeredPort).build(); 36 | } 37 | 38 | public String getName() { 39 | return name; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/requirements/PortsRequirement.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.requirements; 2 | 3 | import com.containersolutions.mesos.scheduler.config.MesosConfigProperties; 4 | import com.containersolutions.mesos.scheduler.config.ResourcePortConfigProperties; 5 | import org.apache.mesos.Protos; 6 | import org.springframework.util.NumberUtils; 7 | 8 | import java.util.*; 9 | import java.util.concurrent.ConcurrentLinkedQueue; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.IntStream; 12 | 13 | public class PortsRequirement implements ResourceRequirement { 14 | private final MesosConfigProperties mesosConfig; 15 | 16 | public PortsRequirement(MesosConfigProperties mesosConfig) { 17 | this.mesosConfig = mesosConfig; 18 | } 19 | 20 | @Override 21 | public OfferEvaluation check(String requirement, String taskId, Protos.Offer offer) { 22 | Queue unprivilegedPorts = new ConcurrentLinkedQueue<>(); 23 | Queue priviledgedPorts = new ConcurrentLinkedQueue<>(); 24 | Map fixedPorts = new HashMap<>(); 25 | 26 | Set> ports = mesosConfig.getResources().getPorts().entrySet(); 27 | ports.forEach(kv -> { 28 | String hostPort = kv.getValue().getHost(); 29 | NameAndContainerPort nameAndContainerPort = new NameAndContainerPort(kv.getKey(), kv.getValue().getContainer()); 30 | if (hostPort.equalsIgnoreCase("ANY") || hostPort.equalsIgnoreCase("UNPRIVILEGED")) { 31 | unprivilegedPorts.add(nameAndContainerPort); 32 | } else if (hostPort.equalsIgnoreCase("PRIVILEGED")) { 33 | priviledgedPorts.add(nameAndContainerPort); 34 | } else { 35 | fixedPorts.put(NumberUtils.parseNumber(hostPort, Integer.class), nameAndContainerPort); 36 | } 37 | }); 38 | 39 | List portMappings = offer.getResourcesList().stream() 40 | .filter(resource -> resource.getName().equals("ports")) 41 | .flatMap(resource -> resource.getRanges().getRangeList().stream()) 42 | .flatMapToInt(range -> IntStream.rangeClosed((int) range.getBegin(), (int) range.getEnd())) 43 | .sorted() 44 | .mapToObj(offeredPort -> { 45 | if (!unprivilegedPorts.isEmpty() && offeredPort > 1024) { 46 | return unprivilegedPorts.remove().toPortMapping(offeredPort); 47 | } else if (!priviledgedPorts.isEmpty() && offeredPort <= 1024) { 48 | return priviledgedPorts.remove().toPortMapping(offeredPort); 49 | } else if (fixedPorts.containsKey(offeredPort)) { 50 | return fixedPorts.remove(offeredPort).toPortMapping(offeredPort); 51 | } 52 | return null; 53 | }) 54 | .filter(Objects::nonNull) 55 | .limit(ports.size()) 56 | .collect(Collectors.toList()); 57 | 58 | if (portMappings.size() == ports.size()) { 59 | return OfferEvaluation.accept( 60 | requirement, 61 | taskId, 62 | offer, 63 | portMappings.stream().collect(Collectors.toMap(PortMapping::envName, PortMapping::envValue)), 64 | portMappings, 65 | Collections.emptyList(), 66 | Protos.Resource.newBuilder() 67 | .setType(Protos.Value.Type.RANGES) 68 | .setName("ports") 69 | .setRanges(Protos.Value.Ranges.newBuilder().addAllRange( 70 | portMappings.stream().map(PortMapping::toRange).collect(Collectors.toList()) 71 | )) 72 | .build() 73 | ); 74 | } 75 | return OfferEvaluation.decline(requirement, taskId, offer, null); 76 | } 77 | 78 | private static class NameAndContainerPort { 79 | String name; 80 | int containerPort; 81 | 82 | public NameAndContainerPort(String name, int containerPort) { 83 | this.name = name; 84 | this.containerPort = containerPort; 85 | } 86 | 87 | PortMapping toPortMapping(int offeredPort) { 88 | return new PortMapping(name, offeredPort, containerPort); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/requirements/ResourceRequirement.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.requirements; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | @FunctionalInterface 6 | public interface ResourceRequirement { 7 | OfferEvaluation check(String requirement, String taskId, Protos.Offer offer); 8 | 9 | static double scalarSum(Protos.Offer offer, String name) { 10 | return offer.getResourcesList().stream().filter(resource -> resource.getName().equals(name)).mapToDouble(resource -> resource.getScalar().getValue()).sum(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/requirements/RoleRequirement.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.requirements; 2 | 3 | import com.containersolutions.mesos.scheduler.config.MesosConfigProperties; 4 | import org.apache.mesos.Protos; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | public class RoleRequirement implements ResourceRequirement { 12 | private final MesosConfigProperties mesosConfig; 13 | 14 | public RoleRequirement(MesosConfigProperties mesosConfig) { 15 | this.mesosConfig = mesosConfig; 16 | } 17 | 18 | @Override 19 | public OfferEvaluation check(String requirement, String taskId, Protos.Offer offer) { 20 | List roleResources = offer.getResourcesList().stream() 21 | .filter(Protos.Resource::hasRole) 22 | .filter(resource -> resource.getRole().equals(mesosConfig.getRole())) 23 | .collect(Collectors.toList()); 24 | if (!roleResources.isEmpty()) { 25 | return OfferEvaluation.accept( 26 | requirement, 27 | taskId, 28 | offer, 29 | Collections.emptyMap(), 30 | Collections.emptyList(), 31 | Collections.emptyList(), 32 | roleResources 33 | ); 34 | } 35 | return OfferEvaluation.decline(requirement, taskId, offer, "No resources for role " + mesosConfig.getRole()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/requirements/VolumeMapping.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.requirements; 2 | 3 | public class VolumeMapping { 4 | private String hostPath; 5 | private String containerPath; 6 | private boolean readOnly; 7 | 8 | public VolumeMapping(String hostPath, String containerPath, boolean readOnly) { 9 | this.hostPath = hostPath; 10 | this.containerPath = containerPath; 11 | this.readOnly = readOnly; 12 | } 13 | 14 | public String getHostPath() { 15 | return hostPath; 16 | } 17 | 18 | public void setHostPath(String hostPath) { 19 | this.hostPath = hostPath; 20 | } 21 | 22 | public String getContainerPath() { 23 | return containerPath; 24 | } 25 | 26 | public void setContainerPath(String containerPath) { 27 | this.containerPath = containerPath; 28 | } 29 | 30 | public boolean isReadOnly() { 31 | return readOnly; 32 | } 33 | 34 | public void setReadOnly(boolean readOnly) { 35 | this.readOnly = readOnly; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/requirements/VolumesRequirement.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.requirements; 2 | 3 | import com.containersolutions.mesos.scheduler.config.MesosConfigProperties; 4 | import org.apache.mesos.Protos; 5 | 6 | import java.util.Collections; 7 | import java.util.stream.Collectors; 8 | 9 | public class VolumesRequirement implements ResourceRequirement { 10 | private final MesosConfigProperties mesosConfig; 11 | 12 | public VolumesRequirement(MesosConfigProperties mesosConfig) { 13 | this.mesosConfig = mesosConfig; 14 | } 15 | 16 | @Override 17 | public OfferEvaluation check(String requirement, String taskId, Protos.Offer offer) { 18 | return OfferEvaluation.accept( 19 | requirement, 20 | taskId, 21 | offer, 22 | Collections.emptyMap(), 23 | Collections.emptyList(), 24 | mesosConfig.getResources().getVolumes().stream() 25 | .map(properties -> new VolumeMapping( 26 | properties.getHostPath(), 27 | properties.getContainerPath(), 28 | properties.isReadonly())) 29 | .collect(Collectors.toList()) 30 | //TODO: Resources needed? 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/state/StateRepository.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.state; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | import java.util.Optional; 6 | import java.util.Set; 7 | 8 | public interface StateRepository { 9 | Optional getFrameworkID(); 10 | 11 | void store(Protos.TaskInfo taskInfo); 12 | 13 | Set allTaskInfos(); 14 | } 15 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/state/StateRepositoryFile.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.state; 2 | 3 | import com.containersolutions.mesos.scheduler.events.FrameworkRegistreredEvent; 4 | import com.containersolutions.mesos.utils.MesosHelper; 5 | import com.containersolutions.mesos.scheduler.events.StatusUpdateEvent; 6 | import org.apache.commons.logging.Log; 7 | import org.apache.commons.logging.LogFactory; 8 | import org.apache.mesos.Protos; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.context.event.EventListener; 11 | import org.springframework.util.FileCopyUtils; 12 | import org.springframework.util.SerializationUtils; 13 | 14 | import javax.annotation.PreDestroy; 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.util.HashSet; 18 | import java.util.Optional; 19 | import java.util.Set; 20 | import java.util.concurrent.atomic.AtomicReference; 21 | 22 | public class StateRepositoryFile implements StateRepository { 23 | protected final Log logger = LogFactory.getLog(getClass()); 24 | 25 | AtomicReference frameworkId = new AtomicReference<>(); 26 | 27 | File stateHome = new File(".state"); 28 | 29 | @Value("${mesos.framework.name:default}") 30 | String frameworkName; 31 | 32 | @Override 33 | public Optional getFrameworkID() { 34 | if (!stateHome.exists()) { 35 | stateHome.mkdir(); 36 | return Optional.empty(); 37 | } 38 | try { 39 | File frameworkid = new File(frameworkStateHome(), "frameworkid"); 40 | if (!frameworkid.exists()) { 41 | return Optional.empty(); 42 | } 43 | return Optional.of(Protos.FrameworkID.newBuilder().setValue(new String(FileCopyUtils.copyToByteArray(frameworkid))).build()); 44 | } catch (IOException e) { 45 | throw new RuntimeException("Failed to open frameworkId"); 46 | } 47 | } 48 | 49 | @EventListener 50 | public void onFrameworkRegistered(FrameworkRegistreredEvent event) { 51 | logger.debug("Received frameworkId=" + event.getFrameworkID().getValue()); 52 | frameworkId.set(event.getFrameworkID()); 53 | } 54 | 55 | @EventListener 56 | public void onStatusUpdate(StatusUpdateEvent event) { 57 | if (MesosHelper.isTerminalTaskState(event.getTaskStatus().getState())) { 58 | final Set taskInfos = allTaskInfos(); 59 | final Optional taskInfo = taskInfos.stream().filter(task -> task.getTaskId().equals(event.getTaskStatus().getTaskId())).findFirst(); 60 | taskInfo.ifPresent(taskInfos::remove); 61 | save(taskInfos); 62 | } 63 | } 64 | 65 | @PreDestroy 66 | public void onExit() { 67 | frameworkStateHome().delete(); 68 | } 69 | 70 | private void save(Set taskIDs) { 71 | try { 72 | if (!stateHome.exists()) { 73 | logger.info("Creating stateHome directory: " + stateHome.getAbsolutePath()); 74 | stateHome.mkdirs(); 75 | FileCopyUtils.copy(frameworkId.get().toByteArray(), new File(frameworkStateHome(), "frameworkid")); 76 | } 77 | FileCopyUtils.copy(SerializationUtils.serialize(new HashSet<>(taskIDs)), tasksFile()); 78 | } catch (IOException e) { 79 | logger.error("Failed to save taskID list", e); 80 | } 81 | } 82 | 83 | @Override 84 | public void store(Protos.TaskInfo taskInfo) { 85 | final Set taskInfos = allTaskInfos(); 86 | taskInfos.add(taskInfo); 87 | save(taskInfos); 88 | } 89 | 90 | @Override 91 | public Set allTaskInfos() { 92 | if (frameworkId.get() == null) { 93 | logger.warn("Attempted to fetch TaskInfo list before framework has been registered"); 94 | return new HashSet<>(); 95 | } 96 | 97 | final File stateFile = tasksFile(); 98 | try { 99 | return (Set) SerializationUtils.deserialize(FileCopyUtils.copyToByteArray(stateFile)); 100 | } catch (Exception e) { 101 | // logger.error("Failed to read file: " + stateFile.getAbsolutePath(), e); 102 | return new HashSet<>(); 103 | } 104 | } 105 | 106 | private File tasksFile() { 107 | final File stateFile = new File(frameworkStateHome(), "tasks"); 108 | if (!stateFile.exists()) { 109 | try { 110 | stateFile.createNewFile(); 111 | } catch (IOException e) { 112 | throw new RuntimeException("Failed to create state file", e); 113 | } 114 | } 115 | return stateFile; 116 | } 117 | 118 | private File frameworkStateHome() { 119 | File file = new File(stateHome, frameworkName); 120 | if (!file.exists()) { 121 | file.mkdir(); 122 | } 123 | return file; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/scheduler/state/StateRepositoryZookeeper.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.state; 2 | 3 | import com.containersolutions.mesos.scheduler.events.FrameworkRegistreredEvent; 4 | import com.containersolutions.mesos.scheduler.events.FrameworkRemovedEvent; 5 | import com.containersolutions.mesos.scheduler.events.StatusUpdateEvent; 6 | import com.containersolutions.mesos.scheduler.events.TearDownFrameworkEvent; 7 | import org.apache.commons.logging.Log; 8 | import org.apache.commons.logging.LogFactory; 9 | import org.apache.mesos.Protos; 10 | import org.apache.mesos.state.State; 11 | import org.apache.mesos.state.ZooKeeperState; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.context.event.EventListener; 15 | import org.springframework.core.env.Environment; 16 | import org.springframework.util.SerializationUtils; 17 | 18 | import javax.annotation.PreDestroy; 19 | import java.util.HashSet; 20 | import java.util.Optional; 21 | import java.util.Set; 22 | import java.util.concurrent.ExecutionException; 23 | import java.util.concurrent.TimeUnit; 24 | import java.util.concurrent.atomic.AtomicReference; 25 | import java.util.stream.Collectors; 26 | 27 | import static com.containersolutions.mesos.utils.MesosHelper.isTerminalTaskState; 28 | 29 | public class StateRepositoryZookeeper implements StateRepository { 30 | protected final Log logger = LogFactory.getLog(getClass()); 31 | private final AtomicReference frameworkId = new AtomicReference<>(); 32 | 33 | private State zkState; 34 | 35 | private final Environment environment; 36 | 37 | @Value("${mesos.framework.name:default}") 38 | String frameworkName; 39 | 40 | public StateRepositoryZookeeper(Environment environment) { 41 | this.environment = environment; 42 | } 43 | 44 | @Override 45 | public Optional getFrameworkID() { 46 | byte[] value; 47 | try { 48 | value = zkState.fetch("frameworkid").get().value(); 49 | } catch (InterruptedException | ExecutionException e) { 50 | throw new RuntimeException("Failed to fetch framework id from Zookeeper"); 51 | } 52 | if (value.length == 0) { 53 | return Optional.empty(); 54 | } 55 | final Optional frameworkID = Optional.of(Protos.FrameworkID.newBuilder().setValue(((String) SerializationUtils.deserialize(value))).build()); 56 | logger.info("Signing in frameworkName=" + frameworkName + " using FrameworkId=" + frameworkID.map(Protos.FrameworkID::getValue).orElse("NULL")); 57 | return frameworkID; 58 | } 59 | 60 | public void connect() { 61 | zkState = new ZooKeeperState( 62 | environment.getRequiredProperty("mesos.zookeeper.server"), 63 | 1000, 64 | TimeUnit.MILLISECONDS, 65 | "/" + environment.getProperty("mesos.framework.name", "default") 66 | ); 67 | } 68 | 69 | @EventListener 70 | public void onFrameworkRegistered(FrameworkRegistreredEvent event) { 71 | logger.debug("Received frameworkId=" + event.getFrameworkID().getValue()); 72 | frameworkId.set(event.getFrameworkID()); 73 | set("frameworkid", frameworkId.get().getValue()); 74 | } 75 | 76 | @EventListener 77 | public void onStatusUpdate(StatusUpdateEvent event) { 78 | if (isTerminalTaskState(event.getTaskStatus().getState())) { 79 | set("tasks", 80 | allTaskInfos().stream() 81 | .filter(task -> !task.getTaskId().equals(event.getTaskStatus().getTaskId())) 82 | .collect(Collectors.toSet()) 83 | ); 84 | } 85 | } 86 | 87 | @EventListener 88 | public void onFrameworkRemovedEvent(FrameworkRemovedEvent event) throws Exception { 89 | cleanup(); 90 | } 91 | 92 | @EventListener 93 | public void onTearDownFrameworkEvent(TearDownFrameworkEvent event) throws Exception { 94 | cleanup(); 95 | } 96 | 97 | @Override 98 | public void store(Protos.TaskInfo taskInfo) { 99 | logger.debug("Persisting taskInfo for taskId=" + taskInfo.getTaskId().getValue()); 100 | Set taskInfos = allTaskInfos(); 101 | taskInfos.add(taskInfo); 102 | set("tasks", taskInfos); 103 | } 104 | 105 | @Override 106 | public Set allTaskInfos() { 107 | try { 108 | byte[] existingNodes = zkState.fetch("tasks").get().value(); 109 | if (existingNodes.length == 0) { 110 | return new HashSet<>(); 111 | } 112 | return (Set) SerializationUtils.deserialize(existingNodes); 113 | } catch (InterruptedException | ExecutionException e) { 114 | throw new RuntimeException("Failed to get state from Zookeeper", e); 115 | } 116 | 117 | } 118 | 119 | private void set(String key, Object value) { 120 | try { 121 | zkState.store(zkState.fetch(key).get().mutate(SerializationUtils.serialize(value))).get(); 122 | } catch (InterruptedException | ExecutionException e) { 123 | throw new RuntimeException("Unable to set zNode", e); 124 | } 125 | } 126 | 127 | public void cleanup() throws ExecutionException, InterruptedException { 128 | zkState.names().get().forEachRemaining(name -> { 129 | try { 130 | zkState.expunge(zkState.fetch(name).get()).get(); 131 | } catch (InterruptedException | ExecutionException e) { 132 | logger.warn("Failed to delete key " + name); 133 | } 134 | }); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/utils/MesosHelper.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.utils; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | import static java.util.Arrays.asList; 6 | 7 | public class MesosHelper { 8 | 9 | public static boolean isTerminalTaskState(Protos.TaskState state) { 10 | return asList(Protos.TaskState.TASK_FINISHED, Protos.TaskState.TASK_FAILED, Protos.TaskState.TASK_KILLED, Protos.TaskState.TASK_LOST, Protos.TaskState.TASK_ERROR).contains(state); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/java/com/containersolutions/mesos/utils/StreamHelper.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.utils; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Predicate; 5 | 6 | public class StreamHelper { 7 | 8 | /** 9 | * Create a new predicate that will evaluate a test predicate and eventually action consumer if 10 | * test predicate returns negative 11 | * 12 | * @param test predicate for test 13 | * @param action action to be performed on negative test 14 | * @param the type of the input to the predicate 15 | * @return Predicate that will evaluate test predicate and eventually action consumer 16 | */ 17 | public static Predicate onNegative(Predicate test, Consumer action) { 18 | return t -> { 19 | if (test.test(t)) { 20 | return true; 21 | } 22 | action.accept(t); 23 | return false; 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.containersolutions.mesos.config.autoconfigure.MesosSchedulerConfiguration 3 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/test/java/com/containersolutions/mesos/TestHelper.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos; 2 | 3 | import org.apache.mesos.Protos; 4 | 5 | import java.util.function.Consumer; 6 | 7 | public class TestHelper { 8 | public static Protos.Offer createDummyOffer() { 9 | return createDummyOffer(null); 10 | } 11 | 12 | public static Protos.Offer createDummyOffer(Consumer mapper) { 13 | final Protos.Offer.Builder offer = Protos.Offer.newBuilder() 14 | .setId(Protos.OfferID.newBuilder().setValue("offer id")) 15 | .setFrameworkId(Protos.FrameworkID.newBuilder().setValue("framework id")) 16 | .setSlaveId(Protos.SlaveID.newBuilder().setValue("slave id")) 17 | .setHostname("hostname"); 18 | if (mapper != null) { 19 | mapper.accept(offer); 20 | } 21 | return offer.build(); 22 | 23 | } 24 | 25 | public static Protos.TaskInfo createDummyTask(String name) { 26 | return createDummyTask(name, null); 27 | } 28 | 29 | public static Protos.TaskInfo createDummyTask(String name, Consumer mapper) { 30 | Protos.TaskInfo.Builder task = Protos.TaskInfo.newBuilder() 31 | .setName(name) 32 | .setTaskId(Protos.TaskID.newBuilder().setValue(name + " id")) 33 | .setSlaveId(Protos.SlaveID.newBuilder().setValue("slave id")); 34 | if (mapper != null) { 35 | mapper.accept(task); 36 | } 37 | return task.build(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/test/java/com/containersolutions/mesos/config/validation/MesosSchedulerPropertiesValidatorTest.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.config.validation; 2 | 3 | import com.containersolutions.mesos.scheduler.config.*; 4 | import org.junit.Test; 5 | import org.springframework.validation.DirectFieldBindingResult; 6 | import org.springframework.validation.Errors; 7 | 8 | import java.util.Collections; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | public class MesosSchedulerPropertiesValidatorTest { 13 | private MesosConfigProperties config = new MesosConfigProperties(); 14 | 15 | private Errors errors = new DirectFieldBindingResult(config, "mesos"); 16 | 17 | 18 | private MesosSchedulerPropertiesValidator validator = new MesosSchedulerPropertiesValidator(); 19 | 20 | @Test 21 | public void willRejectIfMasterIsEmpty() throws Exception { 22 | config.setMaster(" "); 23 | 24 | validator.validate(config, errors); 25 | assertEquals("master.empty", errors.getFieldError("master").getCode()); 26 | } 27 | 28 | @Test 29 | public void willRejectIfZookeeperIsEmpty() throws Exception { 30 | config.setZookeeper(new ZookeeperConfigProperties()); 31 | config.getZookeeper().setServer(" "); 32 | 33 | validator.validate(config, errors); 34 | assertEquals("zookeeper.server.empty", errors.getFieldError("zookeeper.server").getCode()); 35 | } 36 | 37 | @Test 38 | public void willRejectCountIfNegative() throws Exception { 39 | config.setResources(new ResourcesConfigProperties()); 40 | config.getResources().setCount(-1); 41 | 42 | validator.validate(config, errors); 43 | assertEquals("resources.count.not_positive", errors.getFieldError("resources.count").getCode()); 44 | 45 | 46 | } 47 | @Test 48 | public void willRejectCpuIfNegative() throws Exception { 49 | config.setResources(new ResourcesConfigProperties()); 50 | config.getResources().setCpus(-1.0); 51 | 52 | validator.validate(config, errors); 53 | assertEquals("resources.cpus.not_positive", errors.getFieldError("resources.cpus").getCode()); 54 | 55 | } 56 | 57 | @Test 58 | public void willRejectMemIfNegative() throws Exception { 59 | config.setResources(new ResourcesConfigProperties()); 60 | config.getResources().setMem(-128.0); 61 | 62 | validator.validate(config, errors); 63 | assertEquals("resources.mem.not_positive", errors.getFieldError("resources.mem").getCode()); 64 | } 65 | 66 | @Test 67 | public void willRequireHostPort() throws Exception { 68 | config.setResources(new ResourcesConfigProperties() {{ 69 | setPorts(Collections.singletonMap("test", new ResourcePortConfigProperties() {{ 70 | setContainer(1); 71 | }})); 72 | }}); 73 | 74 | validator.validate(config, errors); 75 | 76 | assertEquals("resources.ports.test.host.empty", errors.getFieldError("resources.ports").getCode()); 77 | } 78 | 79 | @Test 80 | public void willRejectContainerPortWhenNotContainerized() throws Exception { 81 | config.setResources(new ResourcesConfigProperties() {{ 82 | setPorts(Collections.singletonMap("test", new ResourcePortConfigProperties() {{ 83 | setHost("ANY"); 84 | setContainer(1); 85 | }})); 86 | }}); 87 | 88 | validator.validate(config, errors); 89 | 90 | assertEquals("resources.ports.test.container.not_containerized", errors.getFieldError("resources.ports").getCode()); 91 | } 92 | 93 | @Test 94 | public void willNotRejectAGoodConfiguration() throws Exception { 95 | config.setMaster("leader.mesos:5050"); 96 | config.setZookeeper(new ZookeeperConfigProperties() {{ 97 | setServer("leader.mesos:2181"); 98 | }}); 99 | config.setResources(new ResourcesConfigProperties() {{ 100 | setCpus(1.0); 101 | setMem(128); 102 | }}); 103 | 104 | validator.validate(config, errors); 105 | 106 | assertEquals(0, errors.getErrorCount()); 107 | } 108 | } -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/test/java/com/containersolutions/mesos/scheduler/FrameworkInfoFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.scheduler.config.MesosConfigProperties; 4 | import com.containersolutions.mesos.scheduler.state.StateRepository; 5 | import org.apache.mesos.Protos; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.MockitoJUnitRunner; 12 | 13 | import java.util.Optional; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | import static org.mockito.Mockito.when; 17 | 18 | /** 19 | * Tests 20 | */ 21 | @RunWith(MockitoJUnitRunner.class) 22 | public class FrameworkInfoFactoryTest { 23 | public static final String PRINCIPAL = "principal"; 24 | @Mock 25 | MesosConfigProperties mesosConfigProperties; 26 | @Mock 27 | StateRepository stateRepository; 28 | @Mock 29 | CredentialFactory credentialFactory; 30 | @InjectMocks 31 | FrameworkInfoFactory factory; 32 | 33 | @Before 34 | public void before() { 35 | factory.applicationName = ""; 36 | when(mesosConfigProperties.getRole()).thenReturn(""); 37 | when(stateRepository.getFrameworkID()).thenReturn(Optional.empty()); 38 | } 39 | 40 | @Test 41 | public void shouldIncludeCredentialsWhenProvided() { 42 | // When given a valid credential 43 | when(credentialFactory.create()).thenReturn(getCredential(true)); 44 | 45 | // Verify that getPrincipal() contains the principal 46 | assertEquals(PRINCIPAL, factory.create().getPrincipal()); 47 | } 48 | 49 | @Test 50 | public void shouldNotIncludeCredentialsWhenNotProvided() { 51 | // When given a valid credential 52 | when(credentialFactory.create()).thenReturn(getCredential(false)); 53 | 54 | // Verify that getPrincipal() contains the principal 55 | assertEquals("", factory.create().getPrincipal()); 56 | } 57 | 58 | private Protos.Credential getCredential(Boolean auth) { 59 | if (auth) { 60 | return Protos.Credential.newBuilder() 61 | .setPrincipal(PRINCIPAL) 62 | .setSecret("secret") 63 | .build(); 64 | } else { 65 | return Protos.Credential.getDefaultInstance(); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/test/java/com/containersolutions/mesos/scheduler/OfferStrategyFilterTest.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.TestHelper; 4 | import com.containersolutions.mesos.scheduler.requirements.OfferEvaluation; 5 | import com.containersolutions.mesos.scheduler.requirements.ResourceRequirement; 6 | import org.apache.mesos.Protos; 7 | import org.junit.Before; 8 | import org.junit.Ignore; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.MockitoJUnitRunner; 13 | 14 | import java.util.Collections; 15 | import java.util.HashMap; 16 | 17 | import static org.junit.Assert.*; 18 | import static org.mockito.Mockito.*; 19 | 20 | 21 | @RunWith(MockitoJUnitRunner.class) 22 | public class OfferStrategyFilterTest { 23 | private final HashMap resourceRequirements = new HashMap<>(); 24 | private Protos.Offer offer = TestHelper.createDummyOffer(); 25 | @Mock 26 | private ResourceRequirement resourceRequirement; 27 | private OfferStrategyFilter filter = new OfferStrategyFilter(resourceRequirements); 28 | private String taskId = "taskId"; 29 | 30 | @Before 31 | public void setUp() throws Exception { 32 | resourceRequirements.put("requirement 1", resourceRequirement); 33 | } 34 | 35 | @Test 36 | public void willApproveValidOffer() throws Exception { 37 | when(resourceRequirement.check("requirement 1", taskId, offer)).thenReturn(OfferEvaluation.accept("test requirement", taskId, offer, Collections.emptyMap(), Collections.emptyList(), Collections.emptyList())); 38 | 39 | final OfferEvaluation result = filter.evaluate(taskId, offer); 40 | assertTrue(result.isValid()); 41 | assertSame(offer, result.getOffer()); 42 | verify(resourceRequirement).check("requirement 1", taskId, offer); 43 | } 44 | 45 | @Test 46 | public void willRejectInvalidOffer() throws Exception { 47 | when(resourceRequirement.check("requirement 1", taskId, offer)).thenReturn(OfferEvaluation.decline("test requirement", taskId, offer, null)); 48 | assertFalse(filter.evaluate(taskId, offer).isValid()); 49 | verify(resourceRequirement).check("requirement 1", taskId, offer); 50 | } 51 | 52 | @Test 53 | @Ignore("A nice to have that's not ready yet") 54 | public void willNotCheckSecondRequirementIfFirstRejects() throws Exception { 55 | ResourceRequirement decliningRequirement = mock(ResourceRequirement.class); 56 | resourceRequirements.put("requirement 2", decliningRequirement); 57 | 58 | when(resourceRequirement.check("requirement 1", taskId, offer)).thenReturn(OfferEvaluation.decline("requirement 1", taskId, offer, null)); 59 | when(decliningRequirement.check("requirement 2", taskId, offer)).thenReturn(OfferEvaluation.decline("requirement 2", taskId, offer, null)); 60 | 61 | assertFalse(filter.evaluate(taskId, offer).isValid()); 62 | 63 | verifyZeroInteractions(decliningRequirement); 64 | } 65 | 66 | @Test 67 | public void willCheckSecondRequirementIfFirstApproves() throws Exception { 68 | ResourceRequirement approvingRequirement = mock(ResourceRequirement.class); 69 | resourceRequirements.put("requirement 2", approvingRequirement); 70 | 71 | when(resourceRequirement.check("requirement 1", taskId, offer)).thenReturn(OfferEvaluation.accept("requirement 1", taskId, offer, Collections.emptyMap(), Collections.emptyList(), Collections.emptyList())); 72 | when(approvingRequirement.check("requirement 2", taskId, offer)).thenReturn(OfferEvaluation.accept("requirement 2", taskId, offer, Collections.emptyMap(), Collections.emptyList(), Collections.emptyList())); 73 | 74 | assertTrue(filter.evaluate(taskId, offer).isValid()); 75 | 76 | verify(approvingRequirement).check("requirement 2", taskId, offer); 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/test/java/com/containersolutions/mesos/scheduler/TaskReaperTest.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.TestHelper; 4 | import com.containersolutions.mesos.scheduler.events.InstanceCountChangeEvent; 5 | import com.containersolutions.mesos.scheduler.state.StateRepository; 6 | import org.apache.mesos.Protos; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.mockito.junit.MockitoJUnitRunner; 10 | 11 | import java.util.Arrays; 12 | import java.util.Set; 13 | import java.util.stream.Collectors; 14 | 15 | import static org.mockito.Matchers.any; 16 | import static org.mockito.Mockito.*; 17 | 18 | @RunWith(MockitoJUnitRunner.class) 19 | public class TaskReaperTest { 20 | private InstanceCount instanceCount = mock(InstanceCount.class); 21 | 22 | private StateRepository stateRepository = mock(StateRepository.class); 23 | 24 | private UniversalScheduler universalScheduler = mock(UniversalScheduler.class); 25 | 26 | private TaskReaper taskReaper = new TaskReaper(stateRepository, instanceCount, universalScheduler); 27 | 28 | private static Set tasksInfoSet(String... names) { 29 | return Arrays.stream(names) 30 | .map(TestHelper::createDummyTask) 31 | .collect(Collectors.toSet()); 32 | } 33 | 34 | @Test 35 | public void willNotKillTasksWhenCountIsFullfilled() throws Exception { 36 | when(stateRepository.allTaskInfos()).thenReturn(tasksInfoSet("task 1", "task 2")); 37 | when(instanceCount.getCount()).thenReturn(2); 38 | taskReaper.onApplicationEvent(new InstanceCountChangeEvent(1)); 39 | 40 | verifyZeroInteractions(universalScheduler); 41 | } 42 | 43 | @Test 44 | public void willKillTaskWhenScalingDown() throws Exception { 45 | when(stateRepository.allTaskInfos()).thenReturn(tasksInfoSet("task 1", "task 2")); 46 | when(instanceCount.getCount()).thenReturn(1); 47 | taskReaper.onApplicationEvent(new InstanceCountChangeEvent(1)); 48 | 49 | verify(universalScheduler).killTask(any(Protos.TaskID.class)); 50 | } 51 | } -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/test/java/com/containersolutions/mesos/scheduler/UniversalSchedulerTest.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler; 2 | 3 | import com.containersolutions.mesos.TestHelper; 4 | import com.containersolutions.mesos.scheduler.requirements.OfferEvaluation; 5 | import com.containersolutions.mesos.scheduler.state.StateRepository; 6 | import org.apache.mesos.Protos; 7 | import org.apache.mesos.SchedulerDriver; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.ArgumentCaptor; 11 | import org.mockito.junit.MockitoJUnitRunner; 12 | 13 | import java.util.Collections; 14 | import java.util.UUID; 15 | import java.util.function.Supplier; 16 | 17 | import static org.junit.Assert.assertEquals; 18 | import static org.mockito.Mockito.*; 19 | 20 | @RunWith(MockitoJUnitRunner.class) 21 | public class UniversalSchedulerTest { 22 | private OfferStrategyFilter offerStrategyFilter = mock(OfferStrategyFilter.class); 23 | 24 | private TaskInfoFactory taskInfoFactory = mock(TaskInfoFactory.class); 25 | 26 | private SchedulerDriver schedulerDriver = mock(SchedulerDriver.class); 27 | 28 | private Supplier uuidSupplier = mock(Supplier.class); 29 | 30 | private StateRepository stateRepository = mock(StateRepository.class); 31 | 32 | private TaskMaterializer taskMaterializer = mock(TaskMaterializer.class); 33 | 34 | private UniversalScheduler scheduler = new UniversalScheduler(null, offerStrategyFilter, null, uuidSupplier, stateRepository, taskMaterializer, null, null); 35 | 36 | private UUID uuid = UUID.randomUUID(); 37 | private String taskId = uuid.toString(); 38 | 39 | @Test 40 | public void willDeclineInvalidOffers() throws Exception { 41 | Protos.Offer offer = TestHelper.createDummyOffer(); 42 | 43 | when(uuidSupplier.get()).thenReturn(uuid); 44 | when(offerStrategyFilter.evaluate(taskId, offer)).thenReturn(OfferEvaluation.decline("test", taskId, offer, null)); 45 | 46 | scheduler.resourceOffers(schedulerDriver, Collections.singletonList(offer)); 47 | 48 | verify(schedulerDriver).declineOffer(offer.getId()); 49 | verifyNoMoreInteractions(schedulerDriver); 50 | } 51 | 52 | @Test 53 | public void willLaunchTaskFromValidOffer() throws Exception { 54 | Protos.Offer offer = TestHelper.createDummyOffer(); 55 | Protos.TaskInfo task = TestHelper.createDummyTask("task", builder -> builder.setTaskId(Protos.TaskID.newBuilder().setValue(taskId))); 56 | 57 | when(uuidSupplier.get()).thenReturn(uuid); 58 | OfferEvaluation offerEvaluation = OfferEvaluation.accept("test", taskId, offer, Collections.emptyMap(), Collections.emptyList(), Collections.emptyList()); 59 | when(offerStrategyFilter.evaluate(taskId, offer)).thenReturn(offerEvaluation); 60 | when(taskMaterializer.createProposal(offerEvaluation)).thenReturn(new TaskProposal(offer, task)); 61 | 62 | scheduler.resourceOffers(schedulerDriver, Collections.singletonList(offer)); 63 | 64 | verify(schedulerDriver, never()).declineOffer(any(Protos.OfferID.class)); 65 | verify(schedulerDriver).launchTasks(Collections.singleton(offer.getId()), Collections.singleton(task)); 66 | 67 | ArgumentCaptor taskInfoArgumentCaptor = ArgumentCaptor.forClass(Protos.TaskInfo.class); 68 | verify(stateRepository).store(taskInfoArgumentCaptor.capture()); 69 | Protos.TaskInfo taskInfo = taskInfoArgumentCaptor.getValue(); 70 | assertEquals(taskId, taskInfo.getTaskId().getValue()); 71 | } 72 | } -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/test/java/com/containersolutions/mesos/scheduler/requirements/DistinctSlaveRequirementTest.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.requirements; 2 | 3 | import com.containersolutions.mesos.TestHelper; 4 | import com.containersolutions.mesos.scheduler.state.StateRepository; 5 | import org.apache.mesos.Protos; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.junit.MockitoJUnitRunner; 9 | 10 | import java.time.Clock; 11 | import java.time.Instant; 12 | import java.util.Collections; 13 | 14 | import static org.junit.Assert.assertFalse; 15 | import static org.junit.Assert.assertTrue; 16 | import static org.mockito.Mockito.mock; 17 | import static org.mockito.Mockito.when; 18 | 19 | @RunWith(MockitoJUnitRunner.class) 20 | public class DistinctSlaveRequirementTest { 21 | private Clock clock = mock(Clock.class); 22 | 23 | private StateRepository stateRepository = mock(StateRepository.class); 24 | 25 | private DistinctSlaveRequirement requirement = new DistinctSlaveRequirement(clock, stateRepository); 26 | 27 | private String taskId = "taskId"; 28 | private Instant now = Instant.now(); 29 | 30 | @Test 31 | public void willRejectOffersForHostWithTentativeRunningTask() throws Exception { 32 | when(clock.instant()).thenReturn(now); 33 | assertTrue(requirement.check("test requirement", taskId, createOffer("slave 1")).isValid()); 34 | assertFalse(requirement.check("test requirement", taskId, createOffer("slave 1")).isValid()); 35 | } 36 | 37 | @Test 38 | public void willNotAcceptTwoTentativeOffersForSameSlave() throws Exception { 39 | when(clock.instant()).thenReturn(now); 40 | assertTrue(requirement.check("test requirement", taskId, createOffer("slave 1")).isValid()); 41 | assertFalse(requirement.check("test requirement", taskId, createOffer("slave 1")).isValid()); 42 | } 43 | 44 | @Test 45 | public void willAcceptOfferForTentativeHostAfterAWhile() throws Exception { 46 | when(clock.instant()).thenReturn(now); 47 | assertTrue(requirement.check("test requirement", taskId, createOffer("slave 1")).isValid()); 48 | 49 | when(clock.instant()).thenReturn(now.plusSeconds(120)); 50 | assertTrue(requirement.check("test requirement", taskId, createOffer("slave 1")).isValid()); 51 | } 52 | 53 | @Test 54 | public void willRejectOfferForHostWithRunningTask() { 55 | when(stateRepository.allTaskInfos()).thenReturn(Collections.singleton(createTaskInfo("slave 1", taskId))); 56 | 57 | assertFalse(requirement.check("test requirement", taskId, createOffer("slave 1")).isValid()); 58 | } 59 | 60 | private Protos.TaskInfo createTaskInfo(String slave, String taskId) { 61 | return Protos.TaskInfo.newBuilder() 62 | .setName("test") 63 | .setTaskId(Protos.TaskID.newBuilder().setValue(taskId)) 64 | .setSlaveId(Protos.SlaveID.newBuilder().setValue(slave)) 65 | .build(); 66 | } 67 | 68 | private Protos.Offer createOffer(String slave) { 69 | return TestHelper.createDummyOffer(builder -> builder.setSlaveId(Protos.SlaveID.newBuilder().setValue(slave))); 70 | } 71 | } -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/test/java/com/containersolutions/mesos/scheduler/requirements/InstancesCountRequirementTest.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.requirements; 2 | 3 | import com.containersolutions.mesos.TestHelper; 4 | import com.containersolutions.mesos.scheduler.InstanceCount; 5 | import com.containersolutions.mesos.scheduler.state.StateRepository; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.junit.MockitoJUnitRunner; 9 | 10 | import java.util.Collections; 11 | 12 | import static org.junit.Assert.assertFalse; 13 | import static org.junit.Assert.assertTrue; 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.when; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class InstancesCountRequirementTest { 19 | private StateRepository stateRepository = mock(StateRepository.class); 20 | 21 | private InstanceCount instanceCount = mock(InstanceCount.class); 22 | 23 | private InstancesCountRequirement requirement = new InstancesCountRequirement(stateRepository, instanceCount); 24 | 25 | @Test 26 | public void willAcceptOfferWhenCountIsNotReached() throws Exception { 27 | when(instanceCount.getCount()).thenReturn(1); 28 | when(stateRepository.allTaskInfos()).thenReturn(Collections.emptySet()); 29 | assertTrue(requirement.check("test requirement", "taskId 1", TestHelper.createDummyOffer()).isValid()); 30 | } 31 | 32 | @Test 33 | public void willRejectOfferWhenCountIsReached() throws Exception { 34 | when(instanceCount.getCount()).thenReturn(1); 35 | when(stateRepository.allTaskInfos()).thenReturn(Collections.singleton(TestHelper.createDummyTask("task"))); 36 | assertFalse(requirement.check("test requirement", "taskId 2", TestHelper.createDummyOffer()).isValid()); 37 | } 38 | } -------------------------------------------------------------------------------- /spring-boot-starter-mesos/src/test/java/com/containersolutions/mesos/scheduler/requirements/PortsRequirementTest.java: -------------------------------------------------------------------------------- 1 | package com.containersolutions.mesos.scheduler.requirements; 2 | 3 | import com.containersolutions.mesos.scheduler.config.MesosConfigProperties; 4 | import com.containersolutions.mesos.scheduler.config.ResourcePortConfigProperties; 5 | import com.containersolutions.mesos.scheduler.config.ResourcesConfigProperties; 6 | import org.apache.mesos.Protos; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.junit.MockitoJUnitRunner; 11 | 12 | import static com.containersolutions.mesos.TestHelper.createDummyOffer; 13 | import static junit.framework.TestCase.assertFalse; 14 | import static org.junit.Assert.assertEquals; 15 | import static org.junit.Assert.assertTrue; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class PortsRequirementTest { 19 | 20 | private MesosConfigProperties mesosConfig = new MesosConfigProperties(); 21 | 22 | private PortsRequirement requirement = new PortsRequirement(mesosConfig); 23 | 24 | 25 | @Before 26 | public void setUp() throws Exception { 27 | mesosConfig.setResources(new ResourcesConfigProperties()); 28 | } 29 | 30 | private ResourcePortConfigProperties createPort(String hostPort) { 31 | ResourcePortConfigProperties port = new ResourcePortConfigProperties(); 32 | port.setHost(hostPort); 33 | return port; 34 | 35 | } 36 | 37 | @Test 38 | public void willChooseDesiredPorts() throws Exception { 39 | mesosConfig.getResources().getPorts().put("a", createPort("PRIVILEGED")); 40 | mesosConfig.getResources().getPorts().put("b", createPort("UNPRIVILEGED")); 41 | mesosConfig.getResources().getPorts().put("c", createPort("ANY")); 42 | mesosConfig.getResources().getPorts().put("d", createPort("1005")); 43 | mesosConfig.getResources().getPorts().put("e", createPort("3333")); 44 | 45 | final OfferEvaluation ports = requirement.check("test", "taskId", createDummyOffer(builder -> { 46 | builder.addResources(Protos.Resource.newBuilder() 47 | .setName("ports") 48 | .setType(Protos.Value.Type.RANGES) 49 | .setRanges(Protos.Value.Ranges.newBuilder().addRange(Protos.Value.Range.newBuilder().setBegin(1000).setEnd(2000))) 50 | ); 51 | builder.addResources(Protos.Resource.newBuilder() 52 | .setName("ports") 53 | .setType(Protos.Value.Type.RANGES) 54 | .setRanges(Protos.Value.Ranges.newBuilder().addRange(Protos.Value.Range.newBuilder().setBegin(3000).setEnd(4000))) 55 | ); 56 | })); 57 | 58 | assertTrue(ports.isValid()); 59 | assertEquals(1000, ports.getResources().get(0).getRanges().getRange(0).getBegin()); 60 | assertEquals(5, ports.getEnvironmentVariables().size()); 61 | assertEquals(1025, ports.getResources().get(0).getRanges().getRange(2).getBegin()); 62 | assertEquals("1000", ports.getEnvironmentVariables().get("A")); 63 | assertEquals("1025", ports.getEnvironmentVariables().get("B")); 64 | assertEquals("1026", ports.getEnvironmentVariables().get("C")); 65 | assertEquals("1005", ports.getEnvironmentVariables().get("D")); 66 | assertEquals("3333", ports.getEnvironmentVariables().get("E")); 67 | } 68 | 69 | @Test 70 | public void willRejectIfFixedPortIsMissing() throws Exception { 71 | mesosConfig.getResources().getPorts().put("a", createPort("9999")); 72 | 73 | final OfferEvaluation ports = requirement.check("test", "taskId", createDummyOffer(builder -> builder.addResources(Protos.Resource.newBuilder() 74 | .setName("ports") 75 | .setType(Protos.Value.Type.RANGES) 76 | .setRanges(Protos.Value.Ranges.newBuilder().addRange(Protos.Value.Range.newBuilder().setBegin(1000).setEnd(2000))) 77 | ))); 78 | 79 | assertFalse(ports.isValid()); 80 | } 81 | 82 | @Test 83 | public void testwillMapANYPortToFirstUnprivilegedPort() throws Exception { 84 | mesosConfig.getResources().getPorts().put("first", createPort("ANY")); 85 | 86 | final OfferEvaluation ports = requirement.check("test", "taskId", createDummyOffer(builder -> builder.addResources(Protos.Resource.newBuilder() 87 | .setName("ports") 88 | .setType(Protos.Value.Type.RANGES) 89 | .setRanges(Protos.Value.Ranges.newBuilder().addRange(Protos.Value.Range.newBuilder().setBegin(1000).setEnd(2000))) 90 | ))); 91 | assertTrue(ports.isValid()); 92 | assertEquals(1025, ports.getResources().get(0).getRanges().getRange(0).getBegin()); 93 | 94 | } 95 | 96 | @Test 97 | public void testwillMapFixedPortToCorrectPort() throws Exception { 98 | mesosConfig.getResources().getPorts().put("first", createPort("1024")); 99 | 100 | final OfferEvaluation ports = requirement.check("test", "taskId", createDummyOffer(builder -> builder.addResources(Protos.Resource.newBuilder() 101 | .setName("ports") 102 | .setType(Protos.Value.Type.RANGES) 103 | .setRanges(Protos.Value.Ranges.newBuilder().addRange(Protos.Value.Range.newBuilder().setBegin(1000).setEnd(2000))) 104 | ))); 105 | assertTrue(ports.isValid()); 106 | assertEquals(1024, ports.getResources().get(0).getRanges().getRange(0).getBegin()); 107 | 108 | } 109 | 110 | } --------------------------------------------------------------------------------