From c3f5c9e65c2e98f49f25d6289f2401bc43793bfd Mon Sep 17 00:00:00 2001 From: Glenn Renfro Date: Thu, 14 Nov 2019 09:14:36 -0500 Subject: [PATCH] Migrates existing schedules to SCDF 2.3.0 resolves #121 This app pulls in data from the PCFScheduler, AppSummary, AppManifest, the SCDF Task Definition Schema to construct the new schedule for the migration. You will note that for the App Summary, the code was pulled directly from the CF-Deployer, the reason this was done was to obtain additional schedule information required for the migration, without having to make another call to retrieve the app summary (i.e. obtain this detail while its still there). As discussed in the README, you will see that not all properties could be obtained from the available resources and thus have to be populated by a property that user can establish at runtime. However this also means this affects all schedules to be migrated. Updated to allow user to setup maven repository Updated instructions to include how to execute migration Updated docs Updated to support deployer format for tasks Also updated deployers to current release --- .gitignore | 3 + .../.mvn/wrapper/MavenWrapperDownloader.java | 118 +++++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 48337 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 + dataflow-migrate-schedules/README.adoc | 125 ++++++ dataflow-migrate-schedules/mvnw | 286 +++++++++++++ dataflow-migrate-schedules/mvnw.cmd | 161 +++++++ dataflow-migrate-schedules/pom.xml | 133 ++++++ .../scripts/runMigration.sh | 2 + .../MigrateScheduleApplication.java | 29 ++ .../batch/SchedulerProcessor.java | 57 +++ .../batch/SchedulerReader.java | 65 +++ .../batch/SchedulerWriter.java | 57 +++ .../configuration/BatchConfiguration.java | 100 +++++ .../CFMigrateScheduleConfiguration.java | 90 ++++ .../service/AbstractMigrateService.java | 170 ++++++++ .../service/CFMigrateSchedulerService.java | 402 ++++++++++++++++++ .../service/ConvertScheduleInfo.java | 183 ++++++++ .../service/MigrateProperties.java | 191 +++++++++ .../service/MigrateScheduleService.java | 56 +++ .../service/ScheduleProcessedException.java | 28 ++ .../service/SchedulerSkipPolicy.java | 37 ++ .../service/TaskDefinitionRepository.java | 42 ++ .../src/main/resources/application.properties | 1 + .../src/main/resources/banner.txt | 6 + .../migrateschedule/AbstractApplications.java | 166 ++++++++ .../BatchIntegrationTests.java | 128 ++++++ .../migrateschedule/CFGetSchedulesTests.java | 290 +++++++++++++ .../CFMigrateScheduleConfigurationTests.java | 220 ++++++++++ 29 files changed, 3147 insertions(+) create mode 100644 dataflow-migrate-schedules/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 dataflow-migrate-schedules/.mvn/wrapper/maven-wrapper.jar create mode 100644 dataflow-migrate-schedules/.mvn/wrapper/maven-wrapper.properties create mode 100644 dataflow-migrate-schedules/README.adoc create mode 100755 dataflow-migrate-schedules/mvnw create mode 100644 dataflow-migrate-schedules/mvnw.cmd create mode 100644 dataflow-migrate-schedules/pom.xml create mode 100755 dataflow-migrate-schedules/scripts/runMigration.sh create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/MigrateScheduleApplication.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/batch/SchedulerProcessor.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/batch/SchedulerReader.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/batch/SchedulerWriter.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/configuration/BatchConfiguration.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/configuration/CFMigrateScheduleConfiguration.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/AbstractMigrateService.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/CFMigrateSchedulerService.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/ConvertScheduleInfo.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/MigrateProperties.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/MigrateScheduleService.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/ScheduleProcessedException.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/SchedulerSkipPolicy.java create mode 100644 dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/TaskDefinitionRepository.java create mode 100644 dataflow-migrate-schedules/src/main/resources/application.properties create mode 100644 dataflow-migrate-schedules/src/main/resources/banner.txt create mode 100644 dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/AbstractApplications.java create mode 100644 dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/BatchIntegrationTests.java create mode 100644 dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/CFGetSchedulesTests.java create mode 100644 dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/CFMigrateScheduleConfigurationTests.java diff --git a/.gitignore b/.gitignore index 137c178..7cbc838 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ spring-*/src/main/java/META-INF/MANIFEST.MF **/.idea/* rebel.xml **/.vscode/ + +dataflow-migrate-schedules/manifest.yml + diff --git a/dataflow-migrate-schedules/.mvn/wrapper/MavenWrapperDownloader.java b/dataflow-migrate-schedules/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..da7c339 --- /dev/null +++ b/dataflow-migrate-schedules/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,118 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } + catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } + finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } + catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } + catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/dataflow-migrate-schedules/.mvn/wrapper/maven-wrapper.jar b/dataflow-migrate-schedules/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..01e67997377a393fd672c7dcde9dccbedf0cb1e9 GIT binary patch literal 48337 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 + env: + SPRING_APPLICATION_NAME: schedulemigrator + spring_cloud_deployer_cloudfoundry_url: + spring_cloud_deployer_cloudfoundry_org: + spring_cloud_deployer_cloudfoundry_space: + spring_cloud_deployer_cloudfoundry_username: + spring_cloud_deployer_cloudfoundry_password: + spring_cloud_deployer_cloudfoundry_skipSslValidation: + spring_cloud_deployer_cloudfoundry_services: + spring_cloud_scheduler_cloudfoundry_schedulerUrl: + spring_profiles_active: cf + spring.cloud.deployer.cloudfoundry.healthCheckTimeout: 300 + spring.cloud.deployer.cloudfoundry.apiTimeout: 300 + dataflowServerUri: + spring_cloud_task_closecontextEnabled: true + remoteRepositories_repo1_url: https://repo.spring.io/libs-snapshot + services: + - + - +``` +2) From the command line use the cf cli to log into your org and space for which you will migrate your schedules +``` +cf login -a +``` +-or if you need to skip ssl validation- +``` +cf login -a --skip-ssl-validation +``` + +3) Now push the schedulemigrator from the directory where the manifest.yml is present: +``` +cf push +``` + +3) To start the migration: +From the `dataflow-migrate-schedules` directory launch the `runMigration.sh` using the commands below: +``` +chmod +x scripts/runMigration.sh +./scripts/runMigration.sh +``` +=== Picking which schedules to migrate +Use the `scheduleNamesToMigrate` property to specify a comma delimited list of +the schedules you wish to migrate. If you don't specify this property +all schedules will be migrated. For example: +``` +./scripts/runMigration.sh --scheduleNamesToMigrate=task-job3,task-job1 +``` + +=== Limiting one Scheduler to run at a time +If there is a requirement that only one `schedulemigrator` should run at a time you can set the `spring.cloud.task.single-instance-enabled` property to true. This will stop other executions of the schedulemigrator till the currently running instance completes. +To enable this feature use the `runMigration.sh` script as follows. +``` +./scripts/runMigration.sh --spring.cloud.task.single-instance-enabled=true +``` + +=== Configuring Your Deployer Properties +The following deployer properties will affect all schedules to be migrated. +If a property is not set then the default will be used. + +==== Deployer properties to be applied to all migrated schedules: +* healthCheckTimeout +* apiTimeout +* statusTimeout +* stagingTimeout +* startupTimeout +* maximumConcurrentTasks +* javaOpts + +NOTE: Descriptions of these properties can be found : https://github.com/cppwfs/spring-cloud-dataflow-samples/blob/SCDF-121/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/MigrateProperties.java[here] + +=== Supported Databases +The database supported are enumerated https://docs.spring.io/spring-cloud-dataflow/docs/current/reference/htmlsingle/#configuration-local-rdbms[here]. + +=== Previously Pushed Apps +The Cloud Schedule Migration app does not delete previously scheduled applications. +If these apps are no longer needed it is up to the user to delete them. diff --git a/dataflow-migrate-schedules/mvnw b/dataflow-migrate-schedules/mvnw new file mode 100755 index 0000000..8b9da3b --- /dev/null +++ b/dataflow-migrate-schedules/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/dataflow-migrate-schedules/mvnw.cmd b/dataflow-migrate-schedules/mvnw.cmd new file mode 100644 index 0000000..fef5a8f --- /dev/null +++ b/dataflow-migrate-schedules/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/dataflow-migrate-schedules/pom.xml b/dataflow-migrate-schedules/pom.xml new file mode 100644 index 0000000..7af61b8 --- /dev/null +++ b/dataflow-migrate-schedules/pom.xml @@ -0,0 +1,133 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.1.RELEASE + + + io.spring + migrateschedule + 1.0.0.BUILD-SNAPSHOT + Schedule Migrator + Migrates SCDF Schedules to the 2.3 format + + + 1.8 + 2.3.0.RC1 + 2.1.0.RELEASE + + + + + + org.springframework.cloud + spring-cloud-task-dependencies + 2.2.1.RELEASE + pom + import + + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.cloud + spring-cloud-deployer-spi + ${deployer.version} + + + org.springframework.cloud + spring-cloud-deployer-resource-maven + ${deployer.version} + + + org.springframework.boot + spring-boot-starter-batch + + + org.springframework.batch + spring-batch-test + test + + + org.springframework.cloud + spring-cloud-starter-task + + + + com.h2database + h2 + + + org.mariadb.jdbc + mariadb-java-client + + + org.springframework.cloud + spring-cloud-deployer-cloudfoundry + ${deployer.version} + + + org.springframework.cloud + spring-cloud-deployer-resource-docker + ${deployer.version} + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.cloud + spring-cloud-dataflow-core + ${spring-cloud-data-flow.version} + + + org.springframework.cloud + spring-cloud-dataflow-registry + ${spring-cloud-data-flow.version} + + + org.assertj + assertj-core + test + + + org.springframework.integration + spring-integration-core + + + org.springframework.integration + spring-integration-jdbc + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/dataflow-migrate-schedules/scripts/runMigration.sh b/dataflow-migrate-schedules/scripts/runMigration.sh new file mode 100755 index 0000000..b4901cd --- /dev/null +++ b/dataflow-migrate-schedules/scripts/runMigration.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cf rt schedulemigrator "JAVA_OPTS=\"-agentpath:\$PWD/.java-buildpack/open_jdk_jre/bin/jvmkill-1.16.0_RELEASE=printHeapHistogram=1 -Djava.io.tmpdir=\$TMPDIR -XX:ActiveProcessorCount=\$(nproc) -Djava.ext.dirs=\$PWD/.java-buildpack/container_security_provider:\$PWD/.java-buildpack/open_jdk_jre/lib/ext -Djava.security.properties=\$PWD/.java-buildpack/java_security/java.security \$JAVA_OPTS\" && CALCULATED_MEMORY=\$(\$PWD/.java-buildpack/open_jdk_jre/bin/java-buildpack-memory-calculator-3.13.0_RELEASE -totMemory=\$MEMORY_LIMIT -loadedClasses=26092 -poolType=metaspace -stackThreads=250 -vmOptions=\"\$JAVA_OPTS\") && echo JVM Memory Configuration: \$CALCULATED_MEMORY && JAVA_OPTS=\"\$JAVA_OPTS \$CALCULATED_MEMORY\" && MALLOC_ARENA_MAX=2 SERVER_PORT=\$PORT eval exec \$PWD/.java-buildpack/open_jdk_jre/bin/java \$JAVA_OPTS -cp \$PWD/. org.springframework.boot.loader.JarLauncher $*" diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/MigrateScheduleApplication.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/MigrateScheduleApplication.java new file mode 100644 index 0000000..6818f37 --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/MigrateScheduleApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MigrateScheduleApplication { + + public static void main(String[] args) { + SpringApplication.run(MigrateScheduleApplication.class, args); + } + +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/batch/SchedulerProcessor.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/batch/SchedulerProcessor.java new file mode 100644 index 0000000..2cb6e62 --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/batch/SchedulerProcessor.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.batch; + +import io.spring.migrateschedule.service.ConvertScheduleInfo; +import io.spring.migrateschedule.service.MigrateProperties; +import io.spring.migrateschedule.service.MigrateScheduleService; +import io.spring.migrateschedule.service.ScheduleProcessedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.cloud.deployer.spi.scheduler.ScheduleInfo; + +/** + * Enriches the {@link ConvertScheduleInfo} with information obtained from the platform. + * The new name for the schedule is established and the properties as well as commandline args + * so that the SchedulerTaskLauncher can process the entries. + * + * @author Glenn Renfro + */ +public class SchedulerProcessor implements ItemProcessor{ + + private static final Logger logger = LoggerFactory.getLogger(SchedulerProcessor.class); + + private MigrateScheduleService migrateScheduleService; + + private MigrateProperties migrateProperties; + + public SchedulerProcessor(MigrateScheduleService migrateScheduleService, MigrateProperties migrateProperties) { + this.migrateScheduleService = migrateScheduleService; + this.migrateProperties = migrateProperties; + } + + @Override + public ConvertScheduleInfo process(ConvertScheduleInfo scheduleInfo){ + if(scheduleInfo.getScheduleName().contains(migrateProperties.getSchedulerToken())) { + throw new ScheduleProcessedException(scheduleInfo.getScheduleName()); + } + logger.info(String.format("Processing Schedule %s", scheduleInfo.getScheduleName())); + return this.migrateScheduleService.enrichScheduleMetadata(scheduleInfo); + } +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/batch/SchedulerReader.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/batch/SchedulerReader.java new file mode 100644 index 0000000..b673799 --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/batch/SchedulerReader.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.batch; + +import java.util.List; + +import io.spring.migrateschedule.service.ConvertScheduleInfo; +import io.spring.migrateschedule.service.MigrateScheduleService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; +import org.springframework.cloud.deployer.spi.scheduler.ScheduleInfo; +import org.springframework.util.Assert; + +/** + * Retrieves all of the available schedules. + * + * @author Glenn Renfro + */ +public class SchedulerReader extends AbstractItemCountingItemStreamItemReader { + + private static final Logger logger = LoggerFactory.getLogger(SchedulerReader.class); + + private List scheduleInfoList; + + private MigrateScheduleService migrateScheduleService; + + public SchedulerReader(MigrateScheduleService migrateScheduleService) { + Assert.notNull(migrateScheduleService, "convertScheduleService must not be null"); + logger.info("Retrieving schedules from PCF Scheduler"); + this.migrateScheduleService = migrateScheduleService; + } + + @Override + protected ConvertScheduleInfo doRead(){ + return this.scheduleInfoList.get(this.getCurrentItemCount()-1); + } + + @Override + protected void doOpen() { + this.scheduleInfoList = migrateScheduleService.scheduleInfoList(); + this.setMaxItemCount(this.scheduleInfoList.size()); + this.setName("scheduler-reader"); + } + + @Override + protected void doClose(){ + + } +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/batch/SchedulerWriter.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/batch/SchedulerWriter.java new file mode 100644 index 0000000..e480552 --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/batch/SchedulerWriter.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.batch; + +import java.util.List; + +import io.spring.migrateschedule.service.ConvertScheduleInfo; +import io.spring.migrateschedule.service.MigrateScheduleService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.batch.item.ItemWriter; +import org.springframework.cloud.deployer.spi.scheduler.ScheduleInfo; +import org.springframework.cloud.deployer.spi.scheduler.Scheduler; + +/** + * Migrates the original schedule to the new scheduler format required for SCDF + * and stages the SchedulerTaskLauncher. + * + * @author Glenn Renfro + */ +public class SchedulerWriter implements ItemWriter { + + private static final Logger logger = LoggerFactory.getLogger(SchedulerWriter.class); + + private Scheduler scheduler; + + private MigrateScheduleService scheduleService; + + public SchedulerWriter (MigrateScheduleService scheduleService, Scheduler scheduler) { + this.scheduleService = scheduleService; + this.scheduler = scheduler; + } + + @Override + public void write(List list) { + for(ConvertScheduleInfo scheduleInfo : list) { + logger.info(String.format("Migrating Schedule %s ", scheduleInfo.getScheduleName())); + this.scheduleService.migrateSchedule(this.scheduler, scheduleInfo); + logger.info(String.format("Migrated Schedule %s ", scheduleInfo.getScheduleName())); + }; + } +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/configuration/BatchConfiguration.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/configuration/BatchConfiguration.java new file mode 100644 index 0000000..8c7754f --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/configuration/BatchConfiguration.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.configuration; + +import io.spring.migrateschedule.service.ConvertScheduleInfo; +import io.spring.migrateschedule.service.MigrateScheduleService; +import io.spring.migrateschedule.service.MigrateProperties; +import io.spring.migrateschedule.batch.SchedulerProcessor; +import io.spring.migrateschedule.batch.SchedulerReader; +import io.spring.migrateschedule.batch.SchedulerWriter; +import io.spring.migrateschedule.service.ScheduleProcessedException; +import io.spring.migrateschedule.service.SchedulerSkipPolicy; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.deployer.spi.scheduler.ScheduleInfo; +import org.springframework.cloud.deployer.spi.scheduler.Scheduler; +import org.springframework.cloud.task.configuration.EnableTask; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +/** + * @author Glenn Renfro + */ +@Configuration +@EnableBatchProcessing +@EnableTask +public class BatchConfiguration { + + + @Autowired + public JobBuilderFactory jobBuilderFactory; + + @Autowired + public StepBuilderFactory stepBuilderFactory; + + @Bean + public Job importUserJob(Step migrationStep) { + return this.jobBuilderFactory.get("migrationJob") + .incrementer(new RunIdIncrementer()) + .start(migrationStep) + .build(); + } + + @Bean + public Step migrationStep(SchedulerReader itemReader, + SchedulerProcessor schedulerProcessor, SchedulerWriter writer) { + return this.stepBuilderFactory.get("migrationStep") + . chunk(1) + .reader(itemReader) + .processor(schedulerProcessor) + .writer(writer) + .faultTolerant() + .skip(ScheduleProcessedException.class) + .skipPolicy(new SchedulerSkipPolicy()) + .build(); + } + + @Bean + public SchedulerReader itemReader(MigrateScheduleService scheduler) { + return new SchedulerReader(scheduler); + } + + @Bean + public SchedulerWriter itemWriter(Scheduler scheduler, MigrateScheduleService scheduleService) { + return new SchedulerWriter(scheduleService, scheduler); + } + + @Bean + public SchedulerProcessor itemProcessor(MigrateScheduleService migrateScheduleService, MigrateProperties migrateProperties) { + return new SchedulerProcessor(migrateScheduleService, migrateProperties); + } + + @Bean + @ConfigurationProperties + public MigrateProperties converterProperties() { + return new MigrateProperties(); + } +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/configuration/CFMigrateScheduleConfiguration.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/configuration/CFMigrateScheduleConfiguration.java new file mode 100644 index 0000000..9cc5780 --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/configuration/CFMigrateScheduleConfiguration.java @@ -0,0 +1,90 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.configuration; + +import io.pivotal.reactor.scheduler.ReactorSchedulerClient; +import io.pivotal.scheduler.SchedulerClient; +import io.spring.migrateschedule.service.CFMigrateSchedulerService; +import io.spring.migrateschedule.service.MigrateProperties; +import io.spring.migrateschedule.service.MigrateScheduleService; +import io.spring.migrateschedule.service.TaskDefinitionRepository; +import org.cloudfoundry.operations.CloudFoundryOperations; +import org.cloudfoundry.reactor.ConnectionContext; +import org.cloudfoundry.reactor.TokenProvider; +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.deployer.resource.maven.MavenProperties; +import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryConnectionProperties; +import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryTaskLauncher; +import org.springframework.cloud.deployer.spi.scheduler.cloudfoundry.CloudFoundryAppScheduler; +import org.springframework.cloud.deployer.spi.scheduler.cloudfoundry.CloudFoundrySchedulerProperties; +import org.springframework.cloud.deployer.spi.task.TaskLauncher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +/** + * @author Glenn Renfro + */ +@Configuration +@EntityScan({ + "org.springframework.cloud.dataflow.core" +}) +public class CFMigrateScheduleConfiguration { + + @Bean + public ReactorSchedulerClient reactorSchedulerClient(ConnectionContext context, + TokenProvider passwordGrantTokenProvider, + CloudFoundrySchedulerProperties properties) { + return ReactorSchedulerClient.builder() + .connectionContext(context) + .tokenProvider(passwordGrantTokenProvider) + .root(Mono.just(properties.getSchedulerUrl())) + .build(); + } + + @Bean + public CloudFoundrySchedulerProperties cloudFoundrySchedulerProperties() { + return new CloudFoundrySchedulerProperties(); + } + + @Bean + public CFMigrateSchedulerService scheduleService(CloudFoundryOperations cloudFoundryOperations, + SchedulerClient schedulerClient, + CloudFoundryConnectionProperties properties, MigrateProperties migrateProperties, + TaskDefinitionRepository taskDefinitionRepository, MavenProperties mavenProperties) { + return new CFMigrateSchedulerService(cloudFoundryOperations, + schedulerClient, properties, migrateProperties, taskDefinitionRepository, mavenProperties); + } + + @Bean + public CloudFoundryAppScheduler scheduler(SchedulerClient client, CloudFoundryOperations operations, + CloudFoundryConnectionProperties properties, TaskLauncher taskLauncher, + CloudFoundrySchedulerProperties schedulerProperties) { + return new CloudFoundryAppScheduler(client, operations, properties, (CloudFoundryTaskLauncher) taskLauncher, schedulerProperties); + } + + @Bean + @ConfigurationProperties + public MavenProperties mavenProperties() { + return new MavenProperties(); + } + +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/AbstractMigrateService.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/AbstractMigrateService.java new file mode 100644 index 0000000..7077f79 --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/AbstractMigrateService.java @@ -0,0 +1,170 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.service; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.springframework.cloud.dataflow.core.TaskDefinition; +import org.springframework.cloud.dataflow.registry.support.AppResourceCommon; +import org.springframework.cloud.deployer.resource.maven.MavenProperties; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; + +/** + * Abstract class containing methods that will be required for both Cloud Foundry + * and the Kubernetes Impls. + * + * @author Glenn Renfro + */ +public abstract class AbstractMigrateService implements MigrateScheduleService { + + private final static String DATA_FLOW_URI_KEY = "spring.cloud.dataflow.client.serverUri"; + + private final static String COMMAND_ARGUMENT_PREFIX = "cmdarg."; + + protected final static String APP_PREFIX = "app."; + + protected final static String DEPLOYER_PREFIX = "deployer."; + + protected MigrateProperties migrateProperties; + + private TaskDefinitionRepository taskDefinitionRepository; + + private MavenProperties mavenProperties; + + public AbstractMigrateService(MigrateProperties migrateProperties, TaskDefinitionRepository taskDefinitionRepository, MavenProperties mavenProperties) { + this.migrateProperties = migrateProperties; + this.taskDefinitionRepository = taskDefinitionRepository; + this.mavenProperties = mavenProperties; + } + + public TaskDefinition findTaskDefinitionByName(String taskDefinitionName) { + return this.taskDefinitionRepository.findByTaskName(taskDefinitionName); + } + + protected String getSchedulePrefixDefinitionName(String taskDefinitionName) { + return this.migrateProperties.getSchedulerToken() + taskDefinitionName; + } + + /** + * Retain only properties that are meant for the scheduler of a given task(those + * that start with {@code scheduler.}and qualify all + * property values with the {@code spring.cloud.scheduler.} prefix. + * + * @param input the scheduler properties + * @return scheduler properties for the task + */ + protected static Map extractAndQualifySchedulerProperties(Map input) { + final String prefix = "spring.cloud.scheduler."; + + Map result = new TreeMap<>(input).entrySet().stream() + .filter(kv -> kv.getKey().startsWith(prefix)) + .collect(Collectors.toMap(kv -> kv.getKey(), kv -> kv.getValue(), + (fromWildcard, fromApp) -> fromApp)); + + return result; + } + + /** + * Retrieve the resource for the SchedulerTaskLauncher and verify the URI. + * @return {@link Resource} for the SchedulerTaskLauncher. + */ + protected Resource getTaskLauncherResource() { + final URI url; + try { + new URI(this.migrateProperties.getSchedulerTaskLauncherUrl()); //verify url + } + catch (URISyntaxException uriSyntaxException) { + throw new IllegalArgumentException(uriSyntaxException); + } + AppResourceCommon appResourceCommon = new AppResourceCommon(this.mavenProperties, new DefaultResourceLoader()); + return appResourceCommon.getResource(this.migrateProperties.getSchedulerTaskLauncherUrl()); + } + + /** + * Add the appropriate tags to the command line args so that the SchedulerTaskLauncher can + * extract them. + * @param args the command line args to be tagged. + * @return the tagged command line args. + */ + protected List tagCommandLineArgs(List args) { + List taggedArgs = new ArrayList<>(); + + for(String arg : args) { + if(arg.contains("spring.cloud.task.name")) { + continue; + } + String updatedArg = arg; + if (!arg.startsWith(DATA_FLOW_URI_KEY) && !"--".concat(arg).startsWith(DATA_FLOW_URI_KEY)) { + updatedArg = COMMAND_ARGUMENT_PREFIX + + this.migrateProperties.getTaskLauncherPrefix() + "." + arg; + } + taggedArgs.add(updatedArg); + } + return taggedArgs; + } + + /** + * Add the appropriate tags to the command line args so that the SchedulerTaskLauncher can + * extract them. + * @param appName the name of the application to be associated with the property + * @param appProperties the properties to be tagged + * @param prefix the prefix to mark the property as to be used by the SchedulerTaskLauncher. + * @return the tagged command line args. + */ + protected Map tagProperties(String appName, Map appProperties, String prefix) { + Map taggedAppProperties = new HashMap<>(appProperties.size()); + + for(String key : appProperties.keySet()) { + if(key.contains("spring.cloud.task.name")) { + continue; + } + String updatedKey = key; + if (!key.startsWith(DATA_FLOW_URI_KEY)) { + if (StringUtils.hasText(appName)) { + updatedKey = this.migrateProperties.getTaskLauncherPrefix() + "." + + prefix + appName + "." + key; + } + else { + updatedKey = this.migrateProperties.getTaskLauncherPrefix() + "." + + prefix + key; + } + } + taggedAppProperties.put(updatedKey, appProperties.get(key)); + } + return taggedAppProperties; + } + + /** + * Add the required SchedulerTaskLauncher properties. + * @param properties the map of properties in which to add the SchedulerTaskLauncher properties. + * @return updated properties. + */ + protected Map addSchedulerAppProps(Map properties) { + Map appProperties = new HashMap<>(properties); + appProperties.put("spring.cloud.dataflow.client.serverUri", this.migrateProperties.getDataflowServerUri()); + return appProperties; + } +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/CFMigrateSchedulerService.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/CFMigrateSchedulerService.java new file mode 100644 index 0000000..0495ceb --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/CFMigrateSchedulerService.java @@ -0,0 +1,402 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.pivotal.scheduler.SchedulerClient; +import io.pivotal.scheduler.v1.jobs.ListJobsRequest; +import io.pivotal.scheduler.v1.jobs.ListJobsResponse; +import org.cloudfoundry.operations.CloudFoundryOperations; +import org.cloudfoundry.operations.applications.ApplicationEnvironments; +import org.cloudfoundry.operations.applications.ApplicationManifest; +import org.cloudfoundry.operations.applications.ApplicationSummary; +import org.cloudfoundry.operations.applications.GetApplicationEnvironmentsRequest; +import org.cloudfoundry.operations.applications.GetApplicationManifestRequest; +import org.cloudfoundry.operations.applications.Route; +import org.cloudfoundry.operations.spaces.SpaceSummary; +import org.codehaus.plexus.util.cli.CommandLineUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.dataflow.core.TaskDefinition; +import org.springframework.cloud.deployer.resource.maven.MavenProperties; +import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryConnectionProperties; +import org.springframework.cloud.deployer.spi.core.AppDefinition; +import org.springframework.cloud.deployer.spi.scheduler.ScheduleRequest; +import org.springframework.cloud.deployer.spi.scheduler.Scheduler; +import org.springframework.cloud.deployer.spi.scheduler.SchedulerException; +import org.springframework.cloud.deployer.spi.scheduler.SchedulerPropertyKeys; +import org.springframework.util.StringUtils; + +/** + * Services required to migrate schedules to the 2.3.0 format in Cloud Foundry + * and stage the SchedulerTaskLauncher. + * + * @author Glenn Renfro + */ +public class CFMigrateSchedulerService extends AbstractMigrateService { + + public static final String JAR_LAUNCHER = "org.springframework.boot.loader.JarLauncher"; + + private static final int JAR_LAUNCHER_LENGTH = JAR_LAUNCHER.length(); + + private static final Logger logger = LoggerFactory.getLogger(CFMigrateSchedulerService.class); + + private static final String CLOUD_FOUNDRY_PREFIX = "cloudfoundry"; + + private final static int PCF_PAGE_START_NUM = 1; //First PageNum for PCFScheduler starts at 1. + + private final static String SCHEDULER_SERVICE_ERROR_MESSAGE = "Scheduler Service returned a null response."; + + private CloudFoundryOperations cloudFoundryOperations; + + private SchedulerClient schedulerClient; + + private CloudFoundryConnectionProperties properties; + + + public CFMigrateSchedulerService(CloudFoundryOperations cloudFoundryOperations, + SchedulerClient schedulerClient, + CloudFoundryConnectionProperties properties, MigrateProperties migrateProperties, + TaskDefinitionRepository taskDefinitionRepository, MavenProperties mavenProperties) { + super(migrateProperties, taskDefinitionRepository, mavenProperties); + this.cloudFoundryOperations = cloudFoundryOperations; + this.schedulerClient = schedulerClient; + this.properties = properties; + } + + @Override + public List scheduleInfoList() { + List result = new ArrayList<>(); + int pageCount = getJobPageCount(); + for (int i = PCF_PAGE_START_NUM; i <= pageCount; i++) { + logger.info(String.format("Reading Schedules Page %s of %s ", i, pageCount )); + List scheduleInfoPage = getSchedules(i); + if(scheduleInfoPage == null) { + throw new SchedulerException(SCHEDULER_SERVICE_ERROR_MESSAGE); + } + result.addAll(scheduleInfoPage); + } + Collections.sort(result); + return result; + } + + public List getSchedules(int page) { + Flux applicationSummaries = cacheAppSummaries(); + return this.getSpace(this.properties.getSpace()).flatMap(requestSummary -> { + return this.schedulerClient.jobs().list(ListJobsRequest.builder() + .spaceId(requestSummary.getId()) + .page(page) + .detailed(true).build()); + }) + .flatMapIterable(jobs -> jobs.getResources())// iterate over the resources returned. + .flatMap(job -> { + return getApplication(applicationSummaries, + job.getApplicationId()) // get the application name for each job. + .map(optionalApp -> { + ConvertScheduleInfo scheduleInfo = new ConvertScheduleInfo(); + scheduleInfo.setScheduleProperties(new HashMap<>()); + scheduleInfo.setScheduleName(job.getName()); + scheduleInfo.setTaskDefinitionName(optionalApp.getName()); + String jobCommandLine = job.getCommand(); + String commandArgs = ""; + if (jobCommandLine != null && jobCommandLine.length() > JAR_LAUNCHER_LENGTH) { + int locationOfArgs = job.getCommand().indexOf(JAR_LAUNCHER) + JAR_LAUNCHER_LENGTH; + commandArgs = job.getCommand().substring(locationOfArgs); + } + else { + logger.warn(String.format("Job %s does not have commandArgs associated with it.", job.getName())); + } + if (StringUtils.hasText(commandArgs)) { + try { + scheduleInfo.setCommandLineArgs(Arrays.asList(CommandLineUtils.translateCommandline(commandArgs))); + } + catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + if (job.getJobSchedules() != null) { + scheduleInfo.getScheduleProperties().put(SchedulerPropertyKeys.CRON_EXPRESSION, + job.getJobSchedules().get(0).getExpression()); + } + else { + logger.warn(String.format("Job %s does not have an associated schedule", job.getName())); + } + return scheduleInfo; + }); + }) + .filter(job -> isScheduleMigratable(job.getScheduleName())) + .collectList().block(); + } + + private boolean isScheduleMigratable(String scheduleName) { + boolean result; + if(migrateProperties.getScheduleNamesToMigrate().size() > 0) { + result = migrateProperties.getScheduleNamesToMigrate().contains(scheduleName); + } + else { + result = true; + } + return result; + } + + @Override + public ConvertScheduleInfo enrichScheduleMetadata(ConvertScheduleInfo scheduleInfo) { + logger.info(String.format("Retrieving Properties from application %s for schedule %s", scheduleInfo.getTaskDefinitionName(), scheduleInfo.getScheduleName())); + ApplicationEnvironments environment = this.cloudFoundryOperations.applications(). + getEnvironments(GetApplicationEnvironmentsRequest.builder(). + name(scheduleInfo.getTaskDefinitionName()). + build()). + block(); + + logger.info(String.format("Retrieving ApplicationManifest for application %s for schedule %s", scheduleInfo.getTaskDefinitionName(), scheduleInfo.getScheduleName())); + ApplicationManifest applicationManifest = getApplicationManifest(scheduleInfo.getTaskDefinitionName()); + if(applicationManifest != null) { + addApplicationManifestPropsToConvertScheduleInfo(scheduleInfo, applicationManifest); + } + if (environment != null) { + for (Map.Entry var : environment.getUserProvided().entrySet()) { + scheduleInfo.getScheduleProperties().put(var.getKey(), (String) var.getValue()); + } + } + logger.info(String.format("Tagging command line args for application %s for schedule %s", scheduleInfo.getTaskDefinitionName(), scheduleInfo.getScheduleName())); + List revisedCommandLineArgs = tagCommandLineArgs(scheduleInfo.getCommandLineArgs()); + revisedCommandLineArgs.add("--spring.cloud.scheduler.task.launcher.taskName=" + scheduleInfo.getTaskDefinitionName()); + scheduleInfo.setCommandLineArgs(revisedCommandLineArgs); + Map appProperties = null; + try { + logger.info(String.format("Extracting Spring App Properties for application %s for schedule %s", scheduleInfo.getTaskDefinitionName(), scheduleInfo.getScheduleName())); + appProperties = getSpringAppProperties(scheduleInfo.getScheduleProperties()); + if(appProperties.size() > 0) { + scheduleInfo.setUseSpringApplicationJson(true); + } + } + catch (Exception exception) { + throw new IllegalArgumentException("Unable to parse SPRING_APPLICATION_JSON from USER VARIABLES", exception); + } + logger.info(String.format("Retrieving Task Definition for application %s for schedule %s", scheduleInfo.getTaskDefinitionName(), scheduleInfo.getScheduleName())); + TaskDefinition taskDefinition = findTaskDefinitionByName(appProperties.get("spring.cloud.task.name")); + if (appProperties.size() > 0 && taskDefinition == null) { + throw new IllegalStateException(String.format("The schedule %s contains " + + "properties but the task definition %s does not exist and thus can't be migrated", + scheduleInfo.getScheduleName(), scheduleInfo.getTaskDefinitionName())); + } + logger.info(String.format("Tagging app properties for application %s for schedule %s", scheduleInfo.getTaskDefinitionName(), scheduleInfo.getScheduleName())); + appProperties = tagProperties(taskDefinition.getRegisteredAppName(), appProperties, APP_PREFIX); + Map deployerProperties = tagProperties(taskDefinition.getRegisteredAppName(), + getDeployerProperties(scheduleInfo), DEPLOYER_PREFIX); + appProperties = addSchedulerAppProps(appProperties); + appProperties.putAll(deployerProperties); + scheduleInfo.setAppProperties(appProperties); + return scheduleInfo; + } + + @Override + public void migrateSchedule(Scheduler scheduler, ConvertScheduleInfo scheduleInfo) { + String scheduleName = scheduleInfo.getScheduleName() + "-" + getSchedulePrefixDefinitionName(scheduleInfo.getTaskDefinitionName()); + AppDefinition appDefinition = new AppDefinition(scheduleName, scheduleInfo.getAppProperties()); + logger.info(String.format("Extracting schedule specific properties for schedule %s", scheduleInfo.getScheduleName())); + Map schedulerProperties = extractAndQualifySchedulerProperties(scheduleInfo.getScheduleProperties()); + ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, schedulerProperties, new HashMap<>(), scheduleInfo.getCommandLineArgs(), scheduleName, getTaskLauncherResource()); + logger.info(String.format("Staging ScheduleTaskLauncher and scheduling %s", scheduleInfo.getScheduleName())); + scheduler.schedule(scheduleRequest); + logger.info(String.format("Unscheduling original %s", scheduleInfo.getScheduleName())); + scheduler.unschedule(scheduleInfo.getScheduleName()); + } + + /** + * Retrieves the number of pages that can be returned when retrieving a list of jobs. + * @return an int containing the number of available pages. + */ + private int getJobPageCount() { + ListJobsResponse response = this.getSpace(this.properties.getSpace()).flatMap(requestSummary -> { + return this.schedulerClient.jobs().list(ListJobsRequest.builder() + .spaceId(requestSummary.getId()) + .detailed(false).build()); + }).block(); + if(response == null) { + throw new SchedulerException(SCHEDULER_SERVICE_ERROR_MESSAGE); + } + return response.getPagination().getTotalPages(); + } + + private Map getSpringAppProperties(Map properties) throws Exception { + Map result; + if(properties.containsKey("SPRING_APPLICATION_JSON")) { + result = new ObjectMapper() + .readValue(properties.get("SPRING_APPLICATION_JSON"), Map.class); + } + else { + result = new HashMap<>(); + } + return result; + } + + /** + * Retrieve a {@link Mono} containing a {@link SpaceSummary} for the specified name. + * + * @param spaceName the name of space to search. + * @return the {@link SpaceSummary} associated with the spaceName. + */ + private Mono getSpace(String spaceName) { + return requestSpaces() + .cache() //cache results from first call. + .filter(space -> spaceName.equals(space.getName())) + .singleOrEmpty() + .cast(SpaceSummary.class); + } + + /** + * Retrieve a {@link Flux} containing the available {@link SpaceSummary}s. + * + * @return {@link Flux} of {@link SpaceSummary}s. + */ + private Flux requestSpaces() { + return this.cloudFoundryOperations.spaces() + .list(); + } + + /** + * Retrieve a cached {@link Flux} of {@link ApplicationSummary}s. + */ + private Flux cacheAppSummaries() { + return requestListApplications() + .cache(); //cache results from first call. No need to re-retrieve each time. + } + + /** + * Retrieve a {@link Flux} of {@link ApplicationSummary}s. + */ + private Flux requestListApplications() { + return this.cloudFoundryOperations.applications() + .list(); + } + + /** + * Retrieve a {@link Mono} containing the {@link ApplicationSummary} associated with the appId. + * + * @param applicationSummaries {@link Flux} of {@link ApplicationSummary}s to filter. + * @param appId the id of the {@link ApplicationSummary} to search. + */ + private Mono getApplication(Flux applicationSummaries, + String appId) { + return applicationSummaries + .filter(application -> appId.equals(application.getId())) + .singleOrEmpty(); + } + private ApplicationManifest getApplicationManifest(String appName) { + return this.cloudFoundryOperations.applications() + .getApplicationManifest(GetApplicationManifestRequest + .builder().name(appName).build()) + .block(); + } + + private Map getDeployerProperties(ConvertScheduleInfo scheduleInfo) { + Map result = new HashMap<>(); + if (scheduleInfo.getJavaBuildPack() != null) { + result.put(CLOUD_FOUNDRY_PREFIX + ".buildpack", scheduleInfo.getJavaBuildPack()); + } + if (scheduleInfo.getMemoryInMB() != null) { + result.put(CLOUD_FOUNDRY_PREFIX + ".memory", scheduleInfo.getMemoryInMB() + "m"); + } + if (scheduleInfo.getDiskInMB() != null) { + result.put(CLOUD_FOUNDRY_PREFIX + ".disk", scheduleInfo.getDiskInMB() + "m"); + } + if (scheduleInfo.getApplicationHealthCheck() != null) { + result.put(CLOUD_FOUNDRY_PREFIX + ".health-check", scheduleInfo.getApplicationHealthCheck().getValue()); + } + if (scheduleInfo.getHealthCheckEndPoint() != null) { + result.put(CLOUD_FOUNDRY_PREFIX + ".health-check-http-endpoint", scheduleInfo.getHealthCheckEndPoint()); + } + if (scheduleInfo.getServices() != null && scheduleInfo.getServices().size() > 0) { + result.put(CLOUD_FOUNDRY_PREFIX + ".services", StringUtils.arrayToCommaDelimitedString(scheduleInfo.getServices().toArray())); + } + if (scheduleInfo.getDomains() != null && scheduleInfo.getDomains().size() > 0) { + result.put(CLOUD_FOUNDRY_PREFIX + ".domain", StringUtils.arrayToCommaDelimitedString(scheduleInfo.getDomains().toArray())); + } + if (scheduleInfo.getRoutes() != null && scheduleInfo.getRoutes().size() > 0) { + result.put(CLOUD_FOUNDRY_PREFIX + ".route-path", StringUtils.arrayToCommaDelimitedString(scheduleInfo.getRoutes().toArray())); + } + if (scheduleInfo.getHosts() != null && scheduleInfo.getHosts().size() > 0) { + result.put(CLOUD_FOUNDRY_PREFIX + ".host", StringUtils.arrayToCommaDelimitedString(scheduleInfo.getHosts().toArray())); + } + + // Global deployer properties; + if (this.migrateProperties.getHealthCheckTimeout() != null) { + result.put(CLOUD_FOUNDRY_PREFIX + ".health-check-timeout", this.migrateProperties.getHealthCheckTimeout()); + } + if (this.migrateProperties.getJavaOptions() != null) { + result.put(CLOUD_FOUNDRY_PREFIX + ".javaOpts", this.migrateProperties.getJavaOptions()); + } + if (this.migrateProperties.getApiTimeout() != null) { + result.put(CLOUD_FOUNDRY_PREFIX + ".api-timeout", String.valueOf(this.migrateProperties.getApiTimeout())); + } + if (this.migrateProperties.getStatusTimeout() != null) { + result.put(CLOUD_FOUNDRY_PREFIX + ".status-timeout", String.valueOf(this.migrateProperties.getStatusTimeout())); + } + if (this.migrateProperties.getStagingTimeout() != null) { + result.put(CLOUD_FOUNDRY_PREFIX + ".staging-timeout", String.valueOf(this.migrateProperties.getStagingTimeout())); + } + if (this.migrateProperties.getStartupTimeout() != null) { + result.put(CLOUD_FOUNDRY_PREFIX + ".startup-timeout", String.valueOf(this.migrateProperties.getStartupTimeout())); + } + if (this.migrateProperties.getMaximumConcurrentTasks() != null) { + result.put(CLOUD_FOUNDRY_PREFIX + ".maximum-concurrent-tasks", String.valueOf(this.migrateProperties.getMaximumConcurrentTasks())); + } + + return result; + } + + private ConvertScheduleInfo addApplicationManifestPropsToConvertScheduleInfo(ConvertScheduleInfo scheduleInfo, ApplicationManifest applicationManifest) { + scheduleInfo.setDiskInMB(applicationManifest.getDisk()); + scheduleInfo.setMemoryInMB(applicationManifest.getMemory()); + scheduleInfo.setApplicationHealthCheck(applicationManifest.getHealthCheckType()); + scheduleInfo.setJavaBuildPack(applicationManifest.getBuildpack()); + scheduleInfo.setHealthCheckEndPoint(applicationManifest.getHealthCheckHttpEndpoint()); + if(applicationManifest.getServices() != null && applicationManifest.getServices().size() > 0) { + scheduleInfo.setServices(applicationManifest.getServices()); + } + if(applicationManifest.getDomains() != null && applicationManifest.getDomains().size() > 0) { + scheduleInfo.setDomains(applicationManifest.getDomains()); + } + if(applicationManifest.getRoutes() != null && applicationManifest.getRoutes().size() > 0) { + List routes = new ArrayList<>(); + for (Route route : applicationManifest.getRoutes()) { + routes.add(route.getRoute()); + } + scheduleInfo.setRoutes(routes); + } + if (applicationManifest.getHosts() != null && applicationManifest.getHosts().size() > 0) { + List hosts = new ArrayList<>(); + for (String host : applicationManifest.getHosts()) { + hosts.add(host); + } + scheduleInfo.setHosts(hosts); + } + + return scheduleInfo; + } +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/ConvertScheduleInfo.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/ConvertScheduleInfo.java new file mode 100644 index 0000000..b1c13a2 --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/ConvertScheduleInfo.java @@ -0,0 +1,183 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.cloudfoundry.operations.applications.ApplicationHealthCheck; + +import org.springframework.cloud.deployer.spi.scheduler.ScheduleInfo; + +/** + * A child implementation of {@link ScheduleInfo} that adds additional attributes + * required to migrate to the new schedule format. + * + * @author Glenn Renfro + */ +public class ConvertScheduleInfo extends ScheduleInfo implements Comparable{ + + private List commandLineArgs = new ArrayList<>(); + + private String registeredAppName; + + private Map appProperties = new HashMap<>(); + + private Integer diskInMB; + + private Integer memoryInMB; + + private String javaBuildPack; + + private ApplicationHealthCheck applicationHealthCheck; + + private String healthCheckEndPoint; + + private Integer maximumConcurrentTasks = 20; + + private boolean useSpringApplicationJson; + + private List services; + + private List domains; + + private List routes; + + private List hosts; + + public List getCommandLineArgs() { + return commandLineArgs; + } + + public void setCommandLineArgs(List commandLineArgs) { + this.commandLineArgs = commandLineArgs; + } + + public String getRegisteredAppName() { + return registeredAppName; + } + + public void setRegisteredAppName(String registeredAppName) { + this.registeredAppName = registeredAppName; + } + + public Map getAppProperties() { + return appProperties; + } + + public void setAppProperties(Map appProperties) { + this.appProperties = appProperties; + } + + @Override + public int compareTo(Object o) { + if(o instanceof ConvertScheduleInfo) { + return this.getScheduleName().compareTo(((ConvertScheduleInfo) o).getScheduleName()); + } + throw new IllegalArgumentException("Can only compare Objects of type ConvertScheduleInfo"); + } + + public Integer getDiskInMB() { + return diskInMB; + } + + public void setDiskInMB(Integer diskInMB) { + this.diskInMB = diskInMB; + } + + public Integer getMemoryInMB() { + return memoryInMB; + } + + public void setMemoryInMB(Integer memoryInMB) { + this.memoryInMB = memoryInMB; + } + + public String getJavaBuildPack() { + return javaBuildPack; + } + + public void setJavaBuildPack(String javaBuildPack) { + this.javaBuildPack = javaBuildPack; + } + + public ApplicationHealthCheck getApplicationHealthCheck() { + return applicationHealthCheck; + } + + public void setApplicationHealthCheck(ApplicationHealthCheck applicationHealthCheck) { + this.applicationHealthCheck = applicationHealthCheck; + } + + public boolean isUseSpringApplicationJson() { + return useSpringApplicationJson; + } + + public void setUseSpringApplicationJson(boolean useSpringApplicationJson) { + this.useSpringApplicationJson = useSpringApplicationJson; + } + + public String getHealthCheckEndPoint() { + return healthCheckEndPoint; + } + + public void setHealthCheckEndPoint(String healthCheckEndPoint) { + this.healthCheckEndPoint = healthCheckEndPoint; + } + + public Integer getMaximumConcurrentTasks() { + return maximumConcurrentTasks; + } + + public void setMaximumConcurrentTasks(Integer maximumConcurrentTasks) { + this.maximumConcurrentTasks = maximumConcurrentTasks; + } + + public List getServices() { + return services; + } + + public void setServices(List services) { + this.services = services; + } + + public List getDomains() { + return domains; + } + + public void setDomains(List domains) { + this.domains = domains; + } + + public List getRoutes() { + return routes; + } + + public void setRoutes(List routes) { + this.routes = routes; + } + + public List getHosts() { + return hosts; + } + + public void setHosts(List hosts) { + this.hosts = hosts; + } +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/MigrateProperties.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/MigrateProperties.java new file mode 100644 index 0000000..8469169 --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/MigrateProperties.java @@ -0,0 +1,191 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.service; + +import java.util.ArrayList; +import java.util.List; + +/** + * Allows user to configure the migration application. + * + * @author Glenn Renfro + */ +public class MigrateProperties { + private String schedulerTaskLauncherUrl = "maven://org.springframework.cloud:spring-cloud-dataflow-scheduler-task-launcher:2.3.0.BUILD-SNAPSHOT"; + + /** + * The token for the updated schedules. + */ + private String schedulerToken = "scdf-"; + + /** + * The prefix to attach to the application properties to be sent to the SchedulerTaskLauncher. + */ + private String taskLauncherPrefix = "tasklauncher"; + + private String dataflowServerUri = "http://localhost:9393"; + + /** + * The global Java Options required for the applications to be launched by the schedulerTaskLauncher. + */ + private String javaOptions; + + /** + * The global timeout to be assigned to applications to be launched by the schedulerTaskLauncher. + */ + private String healthCheckTimeout; + + /** + * The global api timeout to be assigned to applications to be launched by scheduleTaskLauncher. + */ + private Long apiTimeout; + + /** + * Timeout for status API operations in milliseconds to be assigned to applications to be launched by scheduleTaskLauncher + */ + private Long statusTimeout; + + /** + * If set, the global override the timeout allocated for staging an app launched by the schedulefTaskLauncher. + */ + private Long stagingTimeout; + + /** + * If set, the global override the timeout allocated for starting an app launched by scheduleTaskLauncher. + */ + private Long startupTimeout; + + /** + * If set, the global override for the maximum number of concurrently running tasks. + */ + private Integer maximumConcurrentTasks; + + /** + * The number of seconds to wait for a schedule to complete. + * This excludes the time it takes to stage the application on Cloud Foundry. + */ + private int scheduleTimeoutInSeconds = 30; + + /** + * Comma delimited list of schedules to migrate. If empty then all schedules will be migrated. + */ + private List scheduleNamesToMigrate = new ArrayList<>(); + + public String getSchedulerTaskLauncherUrl() { + return schedulerTaskLauncherUrl; + } + + public void setSchedulerTaskLauncherUrl(String schedulerTaskLauncherUrl) { + this.schedulerTaskLauncherUrl = schedulerTaskLauncherUrl; + } + + public String getSchedulerToken() { + return schedulerToken; + } + + public void setSchedulerToken(String schedulerToken) { + this.schedulerToken = schedulerToken; + } + + public String getTaskLauncherPrefix() { + return taskLauncherPrefix; + } + + public void setTaskLauncherPrefix(String taskLauncherPrefix) { + this.taskLauncherPrefix = taskLauncherPrefix; + } + + public String getDataflowServerUri() { + return dataflowServerUri; + } + + public void setDataflowServerUri(String dataflowServerUri) { + this.dataflowServerUri = dataflowServerUri; + } + + public int getScheduleTimeoutInSeconds() { + return scheduleTimeoutInSeconds; + } + + public void setScheduleTimeoutInSeconds(int scheduleTimeoutInSeconds) { + this.scheduleTimeoutInSeconds = scheduleTimeoutInSeconds; + } + + public String getJavaOptions() { + return javaOptions; + } + + public void setJavaOptions(String javaOptions) { + this.javaOptions = javaOptions; + } + + public String getHealthCheckTimeout() { + return healthCheckTimeout; + } + + public void setHealthCheckTimeout(String healthCheckTimeout) { + this.healthCheckTimeout = healthCheckTimeout; + } + + public Long getApiTimeout() { + return apiTimeout; + } + + public void setApiTimeout(Long apiTimeout) { + this.apiTimeout = apiTimeout; + } + + public Long getStatusTimeout() { + return statusTimeout; + } + + public void setStatusTimeout(Long statusTimeout) { + this.statusTimeout = statusTimeout; + } + + public Long getStagingTimeout() { + return stagingTimeout; + } + + public void setStagingTimeout(Long stagingTimeout) { + this.stagingTimeout = stagingTimeout; + } + + public Long getStartupTimeout() { + return startupTimeout; + } + + public void setStartupTimeout(Long startupTimeout) { + this.startupTimeout = startupTimeout; + } + + public Integer getMaximumConcurrentTasks() { + return maximumConcurrentTasks; + } + + public void setMaximumConcurrentTasks(Integer maximumConcurrentTasks) { + this.maximumConcurrentTasks = maximumConcurrentTasks; + } + + public List getScheduleNamesToMigrate() { + return scheduleNamesToMigrate; + } + + public void setScheduleNamesToMigrate(List scheduleNamesToMigrate) { + this.scheduleNamesToMigrate = scheduleNamesToMigrate; + } +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/MigrateScheduleService.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/MigrateScheduleService.java new file mode 100644 index 0000000..1da6923 --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/MigrateScheduleService.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.service; + +import java.util.List; + +import org.springframework.cloud.dataflow.core.TaskDefinition; +import org.springframework.cloud.deployer.spi.scheduler.ScheduleInfo; +import org.springframework.cloud.deployer.spi.scheduler.Scheduler; + +/** + * Interface that establishes the method signatures required to migrate + * schedules to the 2.3.0 format as well as stage the application. + */ +public interface MigrateScheduleService { + + /** + * Retrieve all available {@link ScheduleInfo}s. + * @return list of available ScheduleInfos + */ + List scheduleInfoList(); + + /** + * Add properties and commandLine args to the {@link ScheduleInfo} + * @return enriched {@link ConvertScheduleInfo} + */ + ConvertScheduleInfo enrichScheduleMetadata(ConvertScheduleInfo scheduleInfo); + + /** + * Migrates existing schedule to new SCDF schedule. + * @param scheduler the deployer scheduler to build the new schedule. + * @param scheduleInfo the schedule info containing the existing schedule. + */ + void migrateSchedule(Scheduler scheduler, ConvertScheduleInfo scheduleInfo); + + /** + * Retrieve {@link TaskDefinition} for the name provided + * @param taskDefinitionName the name of the {@link TaskDefinition}. + * @return a TaskDefinition + */ + TaskDefinition findTaskDefinitionByName(String taskDefinitionName); +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/ScheduleProcessedException.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/ScheduleProcessedException.java new file mode 100644 index 0000000..669a747 --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/ScheduleProcessedException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.service; + +/** + * Thrown if a schedule has already been processed. + * + * @author Glenn Renfro + */ +public class ScheduleProcessedException extends RuntimeException { + public ScheduleProcessedException(String scheduleName) { + super(String.format("Schedule %s has already been migrated", scheduleName)); + } +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/SchedulerSkipPolicy.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/SchedulerSkipPolicy.java new file mode 100644 index 0000000..3905566 --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/SchedulerSkipPolicy.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.service; + +import org.springframework.batch.core.step.skip.SkipLimitExceededException; +import org.springframework.batch.core.step.skip.SkipPolicy; + +/** + * Establish that there is no max maximum skip count if {@link ScheduleProcessedException} is thrown. + * + * @author Glenn Renfro + * + */ +public class SchedulerSkipPolicy implements SkipPolicy { + @Override + public boolean shouldSkip(Throwable t, int skipCount) throws SkipLimitExceededException { + boolean result = false; + if(t instanceof ScheduleProcessedException) { + result = true; + } + return result; + } +} diff --git a/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/TaskDefinitionRepository.java b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/TaskDefinitionRepository.java new file mode 100644 index 0000000..878951a --- /dev/null +++ b/dataflow-migrate-schedules/src/main/java/io/spring/migrateschedule/service/TaskDefinitionRepository.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule.service; + +import org.springframework.cloud.dataflow.core.TaskDefinition; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.transaction.annotation.Transactional; + +/** + * Repository to access {@link org.springframework.cloud.dataflow.core.TaskDefinition}s. + * + * @author Michael Minella + * @author Gunnar Hillert + */ +@Transactional +public interface TaskDefinitionRepository extends PagingAndSortingRepository { + + Page findByTaskNameContains(String taskName, Pageable pageable); + + /** + * Performs a findByName query and throws an exception if the name is not found. + * @param name the name of the task definition + * @return The task definition instance or NoSuchTaskDefinitionException if not found. + */ + TaskDefinition findByTaskName(String name); +} diff --git a/dataflow-migrate-schedules/src/main/resources/application.properties b/dataflow-migrate-schedules/src/main/resources/application.properties new file mode 100644 index 0000000..398a1dc --- /dev/null +++ b/dataflow-migrate-schedules/src/main/resources/application.properties @@ -0,0 +1 @@ +logging.level.org.springframework.cloud.task=debug diff --git a/dataflow-migrate-schedules/src/main/resources/banner.txt b/dataflow-migrate-schedules/src/main/resources/banner.txt new file mode 100644 index 0000000..da98c5f --- /dev/null +++ b/dataflow-migrate-schedules/src/main/resources/banner.txt @@ -0,0 +1,6 @@ + ____ ____ ____ _____ ____ _ _ _ __ __ _ _ + / ___| / ___| | _ \ | ___| / ___| ___ | |__ ___ __| | _ _ | | ___ ___ | \/ | (_) __ _ _ __ __ _ | |_ ___ _ __ + \___ \ | | | | | | | |_ \___ \ / __| | '_ \ / _ \ / _` | | | | | | | / _ \ / __| | |\/| | | | / _` | | '__| / _` | | __| / _ \ | '__| + ___) | | |___ | |_| | | _| ___) | | (__ | | | | | __/ | (_| | | |_| | | | | __/ \__ \ | | | | | | | (_| | | | | (_| | | |_ | (_) | | | + |____/ \____| |____/ |_| |____/ \___| |_| |_| \___| \__,_| \__,_| |_| \___| |___/ |_| |_| |_| \__, | |_| \__,_| \__| \___/ |_| + |___/ diff --git a/dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/AbstractApplications.java b/dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/AbstractApplications.java new file mode 100644 index 0000000..10d0517 --- /dev/null +++ b/dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/AbstractApplications.java @@ -0,0 +1,166 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule; + +import org.cloudfoundry.doppler.LogMessage; +import org.cloudfoundry.operations.applications.ApplicationEvent; +import org.cloudfoundry.operations.applications.ApplicationManifest; +import org.cloudfoundry.operations.applications.ApplicationSshEnabledRequest; +import org.cloudfoundry.operations.applications.ApplicationSummary; +import org.cloudfoundry.operations.applications.Applications; +import org.cloudfoundry.operations.applications.CopySourceApplicationRequest; +import org.cloudfoundry.operations.applications.DeleteApplicationRequest; +import org.cloudfoundry.operations.applications.DisableApplicationSshRequest; +import org.cloudfoundry.operations.applications.EnableApplicationSshRequest; +import org.cloudfoundry.operations.applications.GetApplicationEventsRequest; +import org.cloudfoundry.operations.applications.GetApplicationManifestRequest; +import org.cloudfoundry.operations.applications.ListApplicationTasksRequest; +import org.cloudfoundry.operations.applications.LogsRequest; +import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; +import org.cloudfoundry.operations.applications.PushApplicationRequest; +import org.cloudfoundry.operations.applications.RenameApplicationRequest; +import org.cloudfoundry.operations.applications.RestageApplicationRequest; +import org.cloudfoundry.operations.applications.RestartApplicationInstanceRequest; +import org.cloudfoundry.operations.applications.RestartApplicationRequest; +import org.cloudfoundry.operations.applications.RunApplicationTaskRequest; +import org.cloudfoundry.operations.applications.ScaleApplicationRequest; +import org.cloudfoundry.operations.applications.SetApplicationHealthCheckRequest; +import org.cloudfoundry.operations.applications.SetEnvironmentVariableApplicationRequest; +import org.cloudfoundry.operations.applications.StartApplicationRequest; +import org.cloudfoundry.operations.applications.StopApplicationRequest; +import org.cloudfoundry.operations.applications.Task; +import org.cloudfoundry.operations.applications.TerminateApplicationTaskRequest; +import org.cloudfoundry.operations.applications.UnsetEnvironmentVariableApplicationRequest; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public abstract class AbstractApplications implements Applications { + @Override + public Mono copySource(CopySourceApplicationRequest copySourceApplicationRequest) { + return null; + } + + @Override + public Mono delete(DeleteApplicationRequest deleteApplicationRequest) { + return null; + } + + @Override + public Mono disableSsh(DisableApplicationSshRequest disableApplicationSshRequest) { + return null; + } + + @Override + public Mono enableSsh(EnableApplicationSshRequest enableApplicationSshRequest) { + return null; + } + + @Override + public Flux getEvents(GetApplicationEventsRequest getApplicationEventsRequest) { + return null; + } + + @Override + public Flux list() { + return null; + } + + @Override + public Flux listTasks(ListApplicationTasksRequest listApplicationTasksRequest) { + return null; + } + + @Override + public Flux logs(LogsRequest logsRequest) { + return null; + } + + @Override + public Mono push(PushApplicationRequest pushApplicationRequest) { + return null; + } + + @Override + public Mono pushManifest(PushApplicationManifestRequest pushApplicationManifestRequest) { + return null; + } + + @Override + public Mono rename(RenameApplicationRequest renameApplicationRequest) { + return null; + } + + @Override + public Mono restage(RestageApplicationRequest restageApplicationRequest) { + return null; + } + + @Override + public Mono restart(RestartApplicationRequest restartApplicationRequest) { + return null; + } + + @Override + public Mono restartInstance(RestartApplicationInstanceRequest restartApplicationInstanceRequest) { + return null; + } + + @Override + public Mono runTask(RunApplicationTaskRequest runApplicationTaskRequest) { + return null; + } + + @Override + public Mono terminateTask(TerminateApplicationTaskRequest terminateApplicationTaskRequest) { + return null; + } + + @Override + public Mono scale(ScaleApplicationRequest scaleApplicationRequest) { + return null; + } + + @Override + public Mono setEnvironmentVariable(SetEnvironmentVariableApplicationRequest setEnvironmentVariableApplicationRequest) { + return null; + } + + @Override + public Mono setHealthCheck(SetApplicationHealthCheckRequest setApplicationHealthCheckRequest) { + return null; + } + + @Override + public Mono sshEnabled(ApplicationSshEnabledRequest applicationSshEnabledRequest) { + return null; + } + + @Override + public Mono start(StartApplicationRequest startApplicationRequest) { + return null; + } + + @Override + public Mono stop(StopApplicationRequest stopApplicationRequest) { + return null; + } + + @Override + public Mono unsetEnvironmentVariable(UnsetEnvironmentVariableApplicationRequest unsetEnvironmentVariableApplicationRequest) { + return null; + } +} diff --git a/dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/BatchIntegrationTests.java b/dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/BatchIntegrationTests.java new file mode 100644 index 0000000..3c4ac1c --- /dev/null +++ b/dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/BatchIntegrationTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import io.spring.migrateschedule.service.ConvertScheduleInfo; +import io.spring.migrateschedule.configuration.BatchConfiguration; +import io.spring.migrateschedule.service.MigrateScheduleService; +import io.spring.migrateschedule.service.TaskDefinitionRepository; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobInstance; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.test.JobLauncherTestUtils; +import org.springframework.batch.test.context.SpringBatchTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeployerAutoConfiguration; +import org.springframework.cloud.deployer.spi.scheduler.Scheduler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(SpringRunner.class) +@SpringBatchTest +@SpringBootTest +@EnableAutoConfiguration(exclude = {CloudFoundryDeployerAutoConfiguration.class}) +@ContextConfiguration(classes = { BatchIntegrationTests.BatchTestConfiguration.class, BatchConfiguration.class}) +@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, + DirtiesContextTestExecutionListener.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class BatchIntegrationTests { + + public static final String DEFAULT_SCHEDULE_NAME = "defaultScheduleName"; + public static final String DEFAULT_TASK_DEFINITION_NAME = "defaultTaskDefinitionName"; + public static final String DEFAULT_APP_NAME = "defaultAppName"; + + @Autowired + private JobLauncherTestUtils jobLauncherTestUtils; + + @Autowired + private MigrateScheduleService migrateScheduleService; + + @Autowired + private JobExplorer jobExplorer; + + @MockBean + private TaskDefinitionRepository taskDefinitionRepository; + + @MockBean + private Scheduler scheduler; + + @Test + public void baseTest() throws Exception{ + final ArgumentCaptor schedulerArgumentCaptor = ArgumentCaptor.forClass(Scheduler.class); + final ArgumentCaptor scheduleInfoArgumentCaptor = ArgumentCaptor.forClass(ConvertScheduleInfo.class); + verify(this.migrateScheduleService, times(2)).enrichScheduleMetadata(scheduleInfoArgumentCaptor.capture()); + verify(this.migrateScheduleService, times(2)).migrateSchedule(schedulerArgumentCaptor.capture(), scheduleInfoArgumentCaptor.capture()); + assertThat(this.jobExplorer.getJobInstanceCount("migrationJob")).isEqualTo(1); + JobInstance jobInstance = this.jobExplorer.getJobInstances("migrationJob",0, 1).get(0); + List jobExecutions = this.jobExplorer.getJobExecutions(jobInstance); + assertThat(jobExecutions.size()).isEqualTo(1); + assertThat(jobExecutions.get(0).getExitStatus().getExitCode()).isEqualTo("COMPLETED"); + } + + @Configuration + public static class BatchTestConfiguration { + @Bean + public MigrateScheduleService convertScheduleService() { + MigrateScheduleService migrateScheduleService = mock(MigrateScheduleService.class); + ConvertScheduleInfo scheduleInfo = new ConvertScheduleInfo(); + scheduleInfo.setScheduleName(DEFAULT_SCHEDULE_NAME); + scheduleInfo.setTaskDefinitionName(DEFAULT_TASK_DEFINITION_NAME); + scheduleInfo.setScheduleProperties(new HashMap<>()); + scheduleInfo.setRegisteredAppName(DEFAULT_APP_NAME); + List schedules = new ArrayList<>(); + schedules.add(scheduleInfo); + scheduleInfo = new ConvertScheduleInfo(); + scheduleInfo.setScheduleName(DEFAULT_SCHEDULE_NAME + 1); + scheduleInfo.setTaskDefinitionName(DEFAULT_TASK_DEFINITION_NAME + 1); + scheduleInfo.setScheduleProperties(new HashMap<>()); + scheduleInfo.setRegisteredAppName(DEFAULT_APP_NAME + 1); + schedules.add(scheduleInfo); + when(migrateScheduleService.scheduleInfoList()).thenReturn(schedules); + when(migrateScheduleService.enrichScheduleMetadata(any())).thenReturn(scheduleInfo); + return migrateScheduleService; + } + } +} diff --git a/dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/CFGetSchedulesTests.java b/dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/CFGetSchedulesTests.java new file mode 100644 index 0000000..17b5ec7 --- /dev/null +++ b/dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/CFGetSchedulesTests.java @@ -0,0 +1,290 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule; + + +import java.util.ArrayList; +import java.util.List; + +import io.pivotal.scheduler.SchedulerClient; +import io.pivotal.scheduler.v1.jobs.CreateJobRequest; +import io.pivotal.scheduler.v1.jobs.CreateJobResponse; +import io.pivotal.scheduler.v1.jobs.DeleteJobRequest; +import io.pivotal.scheduler.v1.jobs.DeleteJobScheduleRequest; +import io.pivotal.scheduler.v1.jobs.ExecuteJobRequest; +import io.pivotal.scheduler.v1.jobs.ExecuteJobResponse; +import io.pivotal.scheduler.v1.jobs.GetJobRequest; +import io.pivotal.scheduler.v1.jobs.GetJobResponse; +import io.pivotal.scheduler.v1.jobs.Job; +import io.pivotal.scheduler.v1.jobs.JobSchedule; +import io.pivotal.scheduler.v1.jobs.Jobs; +import io.pivotal.scheduler.v1.jobs.ListJobHistoriesRequest; +import io.pivotal.scheduler.v1.jobs.ListJobHistoriesResponse; +import io.pivotal.scheduler.v1.jobs.ListJobScheduleHistoriesRequest; +import io.pivotal.scheduler.v1.jobs.ListJobScheduleHistoriesResponse; +import io.pivotal.scheduler.v1.jobs.ListJobSchedulesRequest; +import io.pivotal.scheduler.v1.jobs.ListJobSchedulesResponse; +import io.pivotal.scheduler.v1.jobs.ListJobsRequest; +import io.pivotal.scheduler.v1.jobs.ListJobsResponse; +import io.pivotal.scheduler.v1.jobs.ScheduleJobRequest; +import io.pivotal.scheduler.v1.jobs.ScheduleJobResponse; +import io.spring.migrateschedule.service.CFMigrateSchedulerService; +import io.spring.migrateschedule.service.ConvertScheduleInfo; +import io.spring.migrateschedule.service.MigrateProperties; +import io.spring.migrateschedule.service.TaskDefinitionRepository; +import org.cloudfoundry.operations.CloudFoundryOperations; +import org.cloudfoundry.operations.applications.ApplicationDetail; +import org.cloudfoundry.operations.applications.ApplicationEnvironments; +import org.cloudfoundry.operations.applications.ApplicationHealthCheck; +import org.cloudfoundry.operations.applications.ApplicationManifest; +import org.cloudfoundry.operations.applications.ApplicationSummary; +import org.cloudfoundry.operations.applications.GetApplicationEnvironmentsRequest; +import org.cloudfoundry.operations.applications.GetApplicationHealthCheckRequest; +import org.cloudfoundry.operations.applications.GetApplicationManifestRequest; +import org.cloudfoundry.operations.applications.GetApplicationRequest; +import org.cloudfoundry.operations.spaces.AllowSpaceSshRequest; +import org.cloudfoundry.operations.spaces.CreateSpaceRequest; +import org.cloudfoundry.operations.spaces.DeleteSpaceRequest; +import org.cloudfoundry.operations.spaces.DisallowSpaceSshRequest; +import org.cloudfoundry.operations.spaces.GetSpaceRequest; +import org.cloudfoundry.operations.spaces.RenameSpaceRequest; +import org.cloudfoundry.operations.spaces.SpaceDetail; +import org.cloudfoundry.operations.spaces.SpaceSshAllowedRequest; +import org.cloudfoundry.operations.spaces.SpaceSummary; +import org.cloudfoundry.operations.spaces.Spaces; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.deployer.resource.maven.MavenProperties; +import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryConnectionProperties; +import org.springframework.cloud.deployer.spi.scheduler.Scheduler; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CFGetSchedulesTests { + + private static final String DEFAULT_SPACE = "TESTSPACE"; + private static final String DEFAULT_APPLICATION_ID = "TEST_APPLICATION_ID"; + private static final String DEFAULT_COMMAND_ARG_PROP = "foo=bar"; + private static final String DEFAULT_SCHEDULE_EXPRESSION = "*/1 * ? * *"; + private static final String DEFAULT_COMMAND_ARG = CFMigrateSchedulerService.JAR_LAUNCHER + " " + DEFAULT_COMMAND_ARG_PROP; + + private CFMigrateSchedulerService cfConvertSchedulerService; + private CloudFoundryOperations cloudFoundryOperations; + private CloudFoundryConnectionProperties cloudFoundryConnectionProperties; + private MigrateProperties migrateProperties; + private TaskDefinitionRepository taskDefinitionRepository; + private SchedulerClient schedulerClient; + private Scheduler scheduler; + + @BeforeEach + public void setup() { + this.cloudFoundryOperations = Mockito.mock(CloudFoundryOperations.class); + this.schedulerClient = Mockito.mock(SchedulerClient.class); + this.cloudFoundryConnectionProperties = new CloudFoundryConnectionProperties(); + this.cloudFoundryConnectionProperties.setSpace(DEFAULT_SPACE); + this.migrateProperties = new MigrateProperties(); + this.taskDefinitionRepository = Mockito.mock(TaskDefinitionRepository.class); + this.scheduler = Mockito.mock(Scheduler.class); + this.cfConvertSchedulerService = new CFMigrateSchedulerService(this.cloudFoundryOperations, + this.schedulerClient, + this.cloudFoundryConnectionProperties, this.migrateProperties, + this.taskDefinitionRepository, new MavenProperties()) ; + } + + + @Test + public void testGetSchedules() { + Mockito.when(this.cloudFoundryOperations.applications()).thenReturn(new TestApplications()); + Mockito.when(this.cloudFoundryOperations.spaces()).thenReturn(new TestSpaces()); + Mockito.when(this.schedulerClient.jobs()).thenReturn(new TestJobs()); + List convertScheduleInfos = this.cfConvertSchedulerService.getSchedules(1); + assertThat(convertScheduleInfos.size()).isEqualTo(2); + + ConvertScheduleInfo convertScheduleInfo = convertScheduleInfos.get(0); + baseTests(convertScheduleInfo,"JOB1", DEFAULT_APPLICATION_ID); + assertThat(convertScheduleInfo.getCommandLineArgs().size()).isEqualTo(1); + assertThat(convertScheduleInfo.getCommandLineArgs().get(0)).isEqualTo(DEFAULT_COMMAND_ARG_PROP); + + convertScheduleInfo = convertScheduleInfos.get(1); + assertThat(convertScheduleInfo.getCommandLineArgs().size()).isEqualTo(0); + baseTests(convertScheduleInfo,"JOB2", DEFAULT_APPLICATION_ID); + } + + private void baseTests(ConvertScheduleInfo convertScheduleInfo, String scheduleName, String taskDefinitionName) { + assertThat(convertScheduleInfo.getScheduleProperties().get("spring.cloud.scheduler.cron.expression")).isEqualTo("*/1 * ? * *"); + assertThat(convertScheduleInfo.getScheduleName()).isEqualTo(scheduleName); + assertThat(convertScheduleInfo.getTaskDefinitionName()).isEqualTo(taskDefinitionName); + } + + private static class TestApplications extends AbstractApplications { + @Override + public Mono get(GetApplicationRequest request) { + return null; + } + + @Override + public Mono getApplicationManifest(GetApplicationManifestRequest request) { + return null; + } + + @Override + public Mono getEnvironments(GetApplicationEnvironmentsRequest request) { + return null; + } + + @Override + public Mono getHealthCheck(GetApplicationHealthCheckRequest request) { + return null; + } + + @Override + public Flux list() { + ApplicationSummary applicationSummary = ApplicationSummary.builder(). + id(DEFAULT_APPLICATION_ID). + diskQuota(1024). + instances(1). + memoryLimit(1024). + name(DEFAULT_APPLICATION_ID). + requestedState("GOOD"). + runningInstances(1). + build(); + Flux applicationSummaries = Flux.just(applicationSummary); + return applicationSummaries; + } + } + + private static class TestSpaces implements Spaces { + + @Override + public Mono allowSsh(AllowSpaceSshRequest request) { + return null; + } + + @Override + public Mono create(CreateSpaceRequest request) { + return null; + } + + @Override + public Mono delete(DeleteSpaceRequest request) { + return null; + } + + @Override + public Mono disallowSsh(DisallowSpaceSshRequest request) { + return null; + } + + @Override + public Mono get(GetSpaceRequest request) { + return null; + } + + @Override + public Flux list() { + SpaceSummary spaceSummary = SpaceSummary.builder().id(DEFAULT_SPACE).name(DEFAULT_SPACE).build(); + Flux spaceSummaries = Flux.just(spaceSummary); + return spaceSummaries; + } + + @Override + public Mono rename(RenameSpaceRequest request) { + return null; + } + + @Override + public Mono sshAllowed(SpaceSshAllowedRequest request) { + return null; + } + } + + public static class TestJobs implements Jobs { + + @Override + public Mono create(CreateJobRequest createJobRequest) { + return null; + } + + @Override + public Mono delete(DeleteJobRequest deleteJobRequest) { + return null; + } + + @Override + public Mono deleteSchedule(DeleteJobScheduleRequest deleteJobScheduleRequest) { + return null; + } + + @Override + public Mono execute(ExecuteJobRequest executeJobRequest) { + return null; + } + + @Override + public Mono get(GetJobRequest getJobRequest) { + return null; + } + + @Override + public Mono list(ListJobsRequest listJobsRequest) { + Job job = Job.builder().id("JOB1").name("JOB1"). + command(DEFAULT_COMMAND_ARG). + applicationId(DEFAULT_APPLICATION_ID). + jobSchedule(JobSchedule.builder(). + expression("*/1 * ? * *"). + build()). + build(); + List jobList = new ArrayList<>(); + jobList.add(job); + + job = Job.builder().id("JOB2").name("JOB2"). + applicationId(DEFAULT_APPLICATION_ID). + jobSchedule(JobSchedule.builder(). + expression(DEFAULT_SCHEDULE_EXPRESSION). + build()). + build(); + jobList.add(job); + ListJobsResponse listJobsResponse = ListJobsResponse.builder().addAllResources(jobList).build(); + return Mono.just(listJobsResponse); + } + + @Override + public Mono listHistories(ListJobHistoriesRequest listJobHistoriesRequest) { + return null; + } + + @Override + public Mono listScheduleHistories(ListJobScheduleHistoriesRequest listJobScheduleHistoriesRequest) { + return null; + } + + @Override + public Mono listSchedules(ListJobSchedulesRequest listJobSchedulesRequest) { + return null; + } + + @Override + public Mono schedule(ScheduleJobRequest scheduleJobRequest) { + return null; + } + } + +} diff --git a/dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/CFMigrateScheduleConfigurationTests.java b/dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/CFMigrateScheduleConfigurationTests.java new file mode 100644 index 0000000..248cd49 --- /dev/null +++ b/dataflow-migrate-schedules/src/test/java/io/spring/migrateschedule/CFMigrateScheduleConfigurationTests.java @@ -0,0 +1,220 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.migrateschedule; + + +import java.util.HashMap; + +import io.pivotal.scheduler.SchedulerClient; +import io.spring.migrateschedule.service.ConvertScheduleInfo; +import io.spring.migrateschedule.service.MigrateProperties; +import io.spring.migrateschedule.service.CFMigrateSchedulerService; +import io.spring.migrateschedule.service.TaskDefinitionRepository; +import org.cloudfoundry.operations.CloudFoundryOperations; +import org.cloudfoundry.operations.applications.ApplicationDetail; +import org.cloudfoundry.operations.applications.ApplicationEnvironments; +import org.cloudfoundry.operations.applications.ApplicationHealthCheck; +import org.cloudfoundry.operations.applications.ApplicationManifest; +import org.cloudfoundry.operations.applications.GetApplicationEnvironmentsRequest; +import org.cloudfoundry.operations.applications.GetApplicationHealthCheckRequest; +import org.cloudfoundry.operations.applications.GetApplicationManifestRequest; +import org.cloudfoundry.operations.applications.GetApplicationRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.dataflow.core.TaskDefinition; +import org.springframework.cloud.deployer.resource.maven.MavenProperties; +import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryConnectionProperties; +import org.springframework.cloud.deployer.spi.scheduler.ScheduleRequest; +import org.springframework.cloud.deployer.spi.scheduler.Scheduler; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class CFMigrateScheduleConfigurationTests { + + public static final String DEFAULT_SCHEDULE_NAME = "defaultScheduleName"; + public static final String DEFAULT_TASK_DEFINITION_NAME = "defaultTaskDefinitionName"; + public static final String DEFAULT_APP_NAME = "defaultAppName"; + public static final String DEFAULT_CMD_ARG = "defaultCmd=WOW"; + public static final String DEFAULT_BUILD_PACK = "defaultBuildPack"; + + private CFMigrateSchedulerService cfConvertSchedulerService; + private CloudFoundryOperations cloudFoundryOperations; + private CloudFoundryConnectionProperties cloudFoundryConnectionProperties; + private MigrateProperties migrateProperties; + private TaskDefinitionRepository taskDefinitionRepository; + private SchedulerClient schedulerClient; + private Scheduler scheduler; + + @BeforeEach + public void setup() { + this.cloudFoundryOperations = Mockito.mock(CloudFoundryOperations.class); + this.schedulerClient = Mockito.mock(SchedulerClient.class); + this.cloudFoundryConnectionProperties = new CloudFoundryConnectionProperties(); + this.migrateProperties = new MigrateProperties(); + this.taskDefinitionRepository = Mockito.mock(TaskDefinitionRepository.class); + this.scheduler = Mockito.mock(Scheduler.class); + this.cfConvertSchedulerService = new CFMigrateSchedulerService(this.cloudFoundryOperations, + this.schedulerClient, + this.cloudFoundryConnectionProperties, this.migrateProperties, + this.taskDefinitionRepository, new MavenProperties()) ; + + } + + @Test + public void testEnrichment() { + ConvertScheduleInfo scheduleInfo = createFoundationConvertScheduleInfo(); + assertThat(scheduleInfo.getAppProperties().keySet().size()).isEqualTo(5); + assertThat(scheduleInfo.getAppProperties().get("tasklauncher.app.defaultAppName.foo")).isEqualTo("bar"); + assertThat(scheduleInfo.getAppProperties().get("spring.cloud.dataflow.client.serverUri")).isEqualTo("http://localhost:9393"); + assertThat(scheduleInfo.getAppProperties().get("tasklauncher.deployer.defaultAppName.cloudfoundry.memory")).isEqualTo("1024m"); + assertThat(scheduleInfo.getAppProperties().get("tasklauncher.deployer.defaultAppName.cloudfoundry.health-check")).isEqualTo("port"); + assertThat(scheduleInfo.getAppProperties().get("tasklauncher.deployer.defaultAppName.cloudfoundry.disk")).isEqualTo("1024m"); + + assertThat(scheduleInfo.getCommandLineArgs().size()).isEqualTo(2); + assertThat(scheduleInfo.getCommandLineArgs().get(0)).isEqualTo("cmdarg.tasklauncher.defaultCmd=WOW"); + assertThat(scheduleInfo.getCommandLineArgs().get(1)).isEqualTo("--spring.cloud.scheduler.task.launcher.taskName=defaultTaskDefinitionName"); + } + + @Test + public void testEnrichmentNoProps() { + ConvertScheduleInfo scheduleInfo = new ConvertScheduleInfo(); + scheduleInfo.setScheduleName(DEFAULT_SCHEDULE_NAME); + scheduleInfo.setTaskDefinitionName(DEFAULT_TASK_DEFINITION_NAME); + scheduleInfo.setScheduleProperties(new HashMap<>()); + scheduleInfo.setRegisteredAppName(DEFAULT_APP_NAME); + Mockito.when(cloudFoundryOperations.applications()).thenReturn(new NoPropertyApplication()); + TaskDefinition taskDefinition = TaskDefinition.TaskDefinitionBuilder + .from(new TaskDefinition("fooTask", "foo")) + .setTaskName(DEFAULT_TASK_DEFINITION_NAME) + .setRegisteredAppName(DEFAULT_APP_NAME) + .build(); + Mockito.when(this.taskDefinitionRepository.findByTaskName(Mockito.any())).thenReturn(taskDefinition); + scheduleInfo = this.cfConvertSchedulerService.enrichScheduleMetadata(scheduleInfo); + assertThat(scheduleInfo.getAppProperties().keySet().size()).isEqualTo(1); + assertThat(scheduleInfo.getAppProperties().get("spring.cloud.dataflow.client.serverUri")).isEqualTo("http://localhost:9393"); + assertThat(scheduleInfo.getCommandLineArgs().size()).isEqualTo(1); + assertThat(scheduleInfo.getCommandLineArgs().get(0)).isEqualTo("--spring.cloud.scheduler.task.launcher.taskName=defaultTaskDefinitionName"); + } + + @Test + public void testNoTaskDefinition() { + ConvertScheduleInfo scheduleInfo = new ConvertScheduleInfo(); + scheduleInfo.setScheduleName(DEFAULT_SCHEDULE_NAME); + scheduleInfo.setTaskDefinitionName(DEFAULT_TASK_DEFINITION_NAME); + scheduleInfo.setScheduleProperties(new HashMap<>()); + scheduleInfo.setRegisteredAppName(DEFAULT_APP_NAME); + scheduleInfo.getCommandLineArgs().add(DEFAULT_CMD_ARG); + Mockito.when(cloudFoundryOperations.applications()).thenReturn(new SinglePropertyApplication()); + assertThrows(IllegalStateException.class, () -> this.cfConvertSchedulerService.enrichScheduleMetadata(scheduleInfo)); + } + + + @Test + public void testMigrate() { + ConvertScheduleInfo scheduleInfo = createFoundationConvertScheduleInfo(); + this.cfConvertSchedulerService.migrateSchedule(this.scheduler, scheduleInfo); + final ArgumentCaptor scheduleRequestArgument = ArgumentCaptor.forClass(ScheduleRequest.class); + final ArgumentCaptor scheduleNameArg = ArgumentCaptor.forClass(String.class); + verify(this.scheduler, times(1)).schedule(scheduleRequestArgument.capture()); + verify(this.scheduler, times(1)).unschedule(scheduleNameArg.capture()); + assertThat(scheduleRequestArgument.getValue().getScheduleName()).isEqualTo("defaultScheduleName-scdf-defaultTaskDefinitionName"); + assertThat(scheduleNameArg.getValue()).isEqualTo(DEFAULT_SCHEDULE_NAME); + } + + private ConvertScheduleInfo createFoundationConvertScheduleInfo() { + ConvertScheduleInfo scheduleInfo = new ConvertScheduleInfo(); + scheduleInfo.setScheduleName(DEFAULT_SCHEDULE_NAME); + scheduleInfo.setTaskDefinitionName(DEFAULT_TASK_DEFINITION_NAME); + scheduleInfo.setScheduleProperties(new HashMap<>()); + scheduleInfo.setRegisteredAppName(DEFAULT_APP_NAME); + scheduleInfo.getCommandLineArgs().add(DEFAULT_CMD_ARG); + Mockito.when(cloudFoundryOperations.applications()).thenReturn(new SinglePropertyApplication()); + TaskDefinition taskDefinition = TaskDefinition.TaskDefinitionBuilder + .from(new TaskDefinition("fooTask", "foo")) + .setTaskName(DEFAULT_TASK_DEFINITION_NAME) + .setRegisteredAppName(DEFAULT_APP_NAME) + .build(); + Mockito.when(this.taskDefinitionRepository.findByTaskName(Mockito.any())).thenReturn(taskDefinition); + return this.cfConvertSchedulerService.enrichScheduleMetadata(scheduleInfo); + } + + public static class SinglePropertyApplication extends AbstractApplications { + @Override + public Mono get(GetApplicationRequest getApplicationRequest) { + ApplicationDetail applicationDetail = ApplicationDetail.builder() + .buildpack(DEFAULT_BUILD_PACK) + .stack("defaultstack") + .diskQuota(1024) + .id("defaultappidone") + .memoryLimit(1024) + .instances(1) + .name(DEFAULT_TASK_DEFINITION_NAME) + .requestedState("requestedState") + .runningInstances(1) + .build(); + return Mono.just(applicationDetail); + } + + @Override + public Mono getEnvironments(GetApplicationEnvironmentsRequest getApplicationEnvironmentsRequest) { + return Mono.just(ApplicationEnvironments.builder().userProvided("SPRING_APPLICATION_JSON", "{\"foo\":\"bar\"}").build()); + } + @Override + public Mono getHealthCheck(GetApplicationHealthCheckRequest getApplicationHealthCheckRequest) { + return Mono.just(ApplicationHealthCheck.PORT); + } + + @Override + public Mono getApplicationManifest(GetApplicationManifestRequest getApplicationManifestRequest) { + return Mono.just(ApplicationManifest.builder().name(DEFAULT_TASK_DEFINITION_NAME) + .disk(1024) + .memory(1024) + .healthCheckType(ApplicationHealthCheck.PORT) + .build()); + } + + } + public static class NoPropertyApplication extends AbstractApplications { + + @Override + public Mono get(GetApplicationRequest getApplicationRequest) { + return Mono.empty(); + } + + @Override + public Mono getEnvironments(GetApplicationEnvironmentsRequest getApplicationEnvironmentsRequest) { + return Mono.empty(); + } + + @Override + public Mono getHealthCheck(GetApplicationHealthCheckRequest getApplicationHealthCheckRequest) { + return Mono.empty(); + } + + @Override + public Mono getApplicationManifest(GetApplicationManifestRequest getApplicationManifestRequest) { + return Mono.empty(); + } + } +}