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
This commit is contained in:
Glenn Renfro
2019-11-14 09:14:36 -05:00
committed by Michael Minella
parent f6a75bac23
commit c3f5c9e65c
29 changed files with 3147 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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<T, C extends ScheduleInfo> implements ItemProcessor<ConvertScheduleInfo, ConvertScheduleInfo>{
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);
}
}

View File

@@ -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<C extends ScheduleInfo> extends AbstractItemCountingItemStreamItemReader<ConvertScheduleInfo> {
private static final Logger logger = LoggerFactory.getLogger(SchedulerReader.class);
private List<ConvertScheduleInfo> 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(){
}
}

View File

@@ -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<C extends ScheduleInfo> implements ItemWriter<ConvertScheduleInfo> {
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<? extends ConvertScheduleInfo> 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()));
};
}
}

View File

@@ -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")
.<ScheduleInfo, ScheduleInfo> chunk(1)
.reader(itemReader)
.processor(schedulerProcessor)
.writer(writer)
.faultTolerant()
.skip(ScheduleProcessedException.class)
.skipPolicy(new SchedulerSkipPolicy())
.build();
}
@Bean
public SchedulerReader<ConvertScheduleInfo> itemReader(MigrateScheduleService scheduler) {
return new SchedulerReader(scheduler);
}
@Bean
public SchedulerWriter<ConvertScheduleInfo> itemWriter(Scheduler scheduler, MigrateScheduleService scheduleService) {
return new SchedulerWriter(scheduleService, scheduler);
}
@Bean
public SchedulerProcessor<ConvertScheduleInfo, ConvertScheduleInfo> itemProcessor(MigrateScheduleService migrateScheduleService, MigrateProperties migrateProperties) {
return new SchedulerProcessor(migrateScheduleService, migrateProperties);
}
@Bean
@ConfigurationProperties
public MigrateProperties converterProperties() {
return new MigrateProperties();
}
}

View File

@@ -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();
}
}

View File

@@ -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 <em>scheduler</em> 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<String, String> extractAndQualifySchedulerProperties(Map<String, String> input) {
final String prefix = "spring.cloud.scheduler.";
Map<String, String> 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<String> tagCommandLineArgs(List<String> args) {
List<String> 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<String, String> tagProperties(String appName, Map<String, String> appProperties, String prefix) {
Map<String, String> 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<String, String> addSchedulerAppProps(Map<String, String> properties) {
Map<String, String> appProperties = new HashMap<>(properties);
appProperties.put("spring.cloud.dataflow.client.serverUri", this.migrateProperties.getDataflowServerUri());
return appProperties;
}
}

View File

@@ -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<ConvertScheduleInfo> scheduleInfoList() {
List<ConvertScheduleInfo> 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<ConvertScheduleInfo> scheduleInfoPage = getSchedules(i);
if(scheduleInfoPage == null) {
throw new SchedulerException(SCHEDULER_SERVICE_ERROR_MESSAGE);
}
result.addAll(scheduleInfoPage);
}
Collections.sort(result);
return result;
}
public List<ConvertScheduleInfo> getSchedules(int page) {
Flux<ApplicationSummary> 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<String, Object> 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<String> revisedCommandLineArgs = tagCommandLineArgs(scheduleInfo.getCommandLineArgs());
revisedCommandLineArgs.add("--spring.cloud.scheduler.task.launcher.taskName=" + scheduleInfo.getTaskDefinitionName());
scheduleInfo.setCommandLineArgs(revisedCommandLineArgs);
Map<String, String> 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<String, String> 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<String, String> 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<String, String> getSpringAppProperties(Map<String, String> properties) throws Exception {
Map<String, String> 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<SpaceSummary> 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<SpaceSummary> requestSpaces() {
return this.cloudFoundryOperations.spaces()
.list();
}
/**
* Retrieve a cached {@link Flux} of {@link ApplicationSummary}s.
*/
private Flux<ApplicationSummary> 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<ApplicationSummary> 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<ApplicationSummary> getApplication(Flux<ApplicationSummary> 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<String, String> getDeployerProperties(ConvertScheduleInfo scheduleInfo) {
Map<String, String> 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<String> routes = new ArrayList<>();
for (Route route : applicationManifest.getRoutes()) {
routes.add(route.getRoute());
}
scheduleInfo.setRoutes(routes);
}
if (applicationManifest.getHosts() != null && applicationManifest.getHosts().size() > 0) {
List<String> hosts = new ArrayList<>();
for (String host : applicationManifest.getHosts()) {
hosts.add(host);
}
scheduleInfo.setHosts(hosts);
}
return scheduleInfo;
}
}

View File

@@ -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<String> commandLineArgs = new ArrayList<>();
private String registeredAppName;
private Map<String, String> 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<String> services;
private List<String> domains;
private List<String> routes;
private List<String> hosts;
public List<String> getCommandLineArgs() {
return commandLineArgs;
}
public void setCommandLineArgs(List<String> commandLineArgs) {
this.commandLineArgs = commandLineArgs;
}
public String getRegisteredAppName() {
return registeredAppName;
}
public void setRegisteredAppName(String registeredAppName) {
this.registeredAppName = registeredAppName;
}
public Map<String, String> getAppProperties() {
return appProperties;
}
public void setAppProperties(Map<String, String> 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<String> getServices() {
return services;
}
public void setServices(List<String> services) {
this.services = services;
}
public List<String> getDomains() {
return domains;
}
public void setDomains(List<String> domains) {
this.domains = domains;
}
public List<String> getRoutes() {
return routes;
}
public void setRoutes(List<String> routes) {
this.routes = routes;
}
public List<String> getHosts() {
return hosts;
}
public void setHosts(List<String> hosts) {
this.hosts = hosts;
}
}

View File

@@ -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<String> 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<String> getScheduleNamesToMigrate() {
return scheduleNamesToMigrate;
}
public void setScheduleNamesToMigrate(List<String> scheduleNamesToMigrate) {
this.scheduleNamesToMigrate = scheduleNamesToMigrate;
}
}

View File

@@ -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<ConvertScheduleInfo> 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);
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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<org.springframework.cloud.dataflow.core.TaskDefinition, String> {
Page<org.springframework.cloud.dataflow.core.TaskDefinition> 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);
}

View File

@@ -0,0 +1 @@
logging.level.org.springframework.cloud.task=debug

View File

@@ -0,0 +1,6 @@
____ ____ ____ _____ ____ _ _ _ __ __ _ _
/ ___| / ___| | _ \ | ___| / ___| ___ | |__ ___ __| | _ _ | | ___ ___ | \/ | (_) __ _ _ __ __ _ | |_ ___ _ __
\___ \ | | | | | | | |_ \___ \ / __| | '_ \ / _ \ / _` | | | | | | | / _ \ / __| | |\/| | | | / _` | | '__| / _` | | __| / _ \ | '__|
___) | | |___ | |_| | | _| ___) | | (__ | | | | | __/ | (_| | | |_| | | | | __/ \__ \ | | | | | | | (_| | | | | (_| | | |_ | (_) | | |
|____/ \____| |____/ |_| |____/ \___| |_| |_| \___| \__,_| \__,_| |_| \___| |___/ |_| |_| |_| \__, | |_| \__,_| \__| \___/ |_|
|___/

View File

@@ -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<Void> copySource(CopySourceApplicationRequest copySourceApplicationRequest) {
return null;
}
@Override
public Mono<Void> delete(DeleteApplicationRequest deleteApplicationRequest) {
return null;
}
@Override
public Mono<Void> disableSsh(DisableApplicationSshRequest disableApplicationSshRequest) {
return null;
}
@Override
public Mono<Void> enableSsh(EnableApplicationSshRequest enableApplicationSshRequest) {
return null;
}
@Override
public Flux<ApplicationEvent> getEvents(GetApplicationEventsRequest getApplicationEventsRequest) {
return null;
}
@Override
public Flux<ApplicationSummary> list() {
return null;
}
@Override
public Flux<Task> listTasks(ListApplicationTasksRequest listApplicationTasksRequest) {
return null;
}
@Override
public Flux<LogMessage> logs(LogsRequest logsRequest) {
return null;
}
@Override
public Mono<Void> push(PushApplicationRequest pushApplicationRequest) {
return null;
}
@Override
public Mono<Void> pushManifest(PushApplicationManifestRequest pushApplicationManifestRequest) {
return null;
}
@Override
public Mono<Void> rename(RenameApplicationRequest renameApplicationRequest) {
return null;
}
@Override
public Mono<Void> restage(RestageApplicationRequest restageApplicationRequest) {
return null;
}
@Override
public Mono<Void> restart(RestartApplicationRequest restartApplicationRequest) {
return null;
}
@Override
public Mono<Void> restartInstance(RestartApplicationInstanceRequest restartApplicationInstanceRequest) {
return null;
}
@Override
public Mono<Task> runTask(RunApplicationTaskRequest runApplicationTaskRequest) {
return null;
}
@Override
public Mono<Void> terminateTask(TerminateApplicationTaskRequest terminateApplicationTaskRequest) {
return null;
}
@Override
public Mono<Void> scale(ScaleApplicationRequest scaleApplicationRequest) {
return null;
}
@Override
public Mono<Void> setEnvironmentVariable(SetEnvironmentVariableApplicationRequest setEnvironmentVariableApplicationRequest) {
return null;
}
@Override
public Mono<Void> setHealthCheck(SetApplicationHealthCheckRequest setApplicationHealthCheckRequest) {
return null;
}
@Override
public Mono<Boolean> sshEnabled(ApplicationSshEnabledRequest applicationSshEnabledRequest) {
return null;
}
@Override
public Mono<Void> start(StartApplicationRequest startApplicationRequest) {
return null;
}
@Override
public Mono<Void> stop(StopApplicationRequest stopApplicationRequest) {
return null;
}
@Override
public Mono<Void> unsetEnvironmentVariable(UnsetEnvironmentVariableApplicationRequest unsetEnvironmentVariableApplicationRequest) {
return null;
}
}

View File

@@ -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<Scheduler> schedulerArgumentCaptor = ArgumentCaptor.forClass(Scheduler.class);
final ArgumentCaptor<ConvertScheduleInfo> 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<JobExecution> 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<ConvertScheduleInfo> 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;
}
}
}

View File

@@ -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<ConvertScheduleInfo> 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<ApplicationDetail> get(GetApplicationRequest request) {
return null;
}
@Override
public Mono<ApplicationManifest> getApplicationManifest(GetApplicationManifestRequest request) {
return null;
}
@Override
public Mono<ApplicationEnvironments> getEnvironments(GetApplicationEnvironmentsRequest request) {
return null;
}
@Override
public Mono<ApplicationHealthCheck> getHealthCheck(GetApplicationHealthCheckRequest request) {
return null;
}
@Override
public Flux<ApplicationSummary> 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<ApplicationSummary> applicationSummaries = Flux.just(applicationSummary);
return applicationSummaries;
}
}
private static class TestSpaces implements Spaces {
@Override
public Mono<Void> allowSsh(AllowSpaceSshRequest request) {
return null;
}
@Override
public Mono<Void> create(CreateSpaceRequest request) {
return null;
}
@Override
public Mono<Void> delete(DeleteSpaceRequest request) {
return null;
}
@Override
public Mono<Void> disallowSsh(DisallowSpaceSshRequest request) {
return null;
}
@Override
public Mono<SpaceDetail> get(GetSpaceRequest request) {
return null;
}
@Override
public Flux<SpaceSummary> list() {
SpaceSummary spaceSummary = SpaceSummary.builder().id(DEFAULT_SPACE).name(DEFAULT_SPACE).build();
Flux<SpaceSummary> spaceSummaries = Flux.just(spaceSummary);
return spaceSummaries;
}
@Override
public Mono<Void> rename(RenameSpaceRequest request) {
return null;
}
@Override
public Mono<Boolean> sshAllowed(SpaceSshAllowedRequest request) {
return null;
}
}
public static class TestJobs implements Jobs {
@Override
public Mono<CreateJobResponse> create(CreateJobRequest createJobRequest) {
return null;
}
@Override
public Mono<Void> delete(DeleteJobRequest deleteJobRequest) {
return null;
}
@Override
public Mono<Void> deleteSchedule(DeleteJobScheduleRequest deleteJobScheduleRequest) {
return null;
}
@Override
public Mono<ExecuteJobResponse> execute(ExecuteJobRequest executeJobRequest) {
return null;
}
@Override
public Mono<GetJobResponse> get(GetJobRequest getJobRequest) {
return null;
}
@Override
public Mono<ListJobsResponse> 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<Job> 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<ListJobHistoriesResponse> listHistories(ListJobHistoriesRequest listJobHistoriesRequest) {
return null;
}
@Override
public Mono<ListJobScheduleHistoriesResponse> listScheduleHistories(ListJobScheduleHistoriesRequest listJobScheduleHistoriesRequest) {
return null;
}
@Override
public Mono<ListJobSchedulesResponse> listSchedules(ListJobSchedulesRequest listJobSchedulesRequest) {
return null;
}
@Override
public Mono<ScheduleJobResponse> schedule(ScheduleJobRequest scheduleJobRequest) {
return null;
}
}
}

View File

@@ -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<ScheduleRequest> scheduleRequestArgument = ArgumentCaptor.forClass(ScheduleRequest.class);
final ArgumentCaptor<String> 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<ApplicationDetail> 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<ApplicationEnvironments> getEnvironments(GetApplicationEnvironmentsRequest getApplicationEnvironmentsRequest) {
return Mono.just(ApplicationEnvironments.builder().userProvided("SPRING_APPLICATION_JSON", "{\"foo\":\"bar\"}").build());
}
@Override
public Mono<ApplicationHealthCheck> getHealthCheck(GetApplicationHealthCheckRequest getApplicationHealthCheckRequest) {
return Mono.just(ApplicationHealthCheck.PORT);
}
@Override
public Mono<ApplicationManifest> 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<ApplicationDetail> get(GetApplicationRequest getApplicationRequest) {
return Mono.empty();
}
@Override
public Mono<ApplicationEnvironments> getEnvironments(GetApplicationEnvironmentsRequest getApplicationEnvironmentsRequest) {
return Mono.empty();
}
@Override
public Mono<ApplicationHealthCheck> getHealthCheck(GetApplicationHealthCheckRequest getApplicationHealthCheckRequest) {
return Mono.empty();
}
@Override
public Mono<ApplicationManifest> getApplicationManifest(GetApplicationManifestRequest getApplicationManifestRequest) {
return Mono.empty();
}
}
}