package org.aravind.oss.kafka.connect.jenkins; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.kafka.connect.data.Schema; import org.apache.kafka.connect.source.SourceRecord; import org.apache.kafka.connect.source.SourceTask; import org.apache.kafka.connect.source.SourceTaskContext; import org.aravind.oss.jenkins.JenkinsClient; import org.aravind.oss.jenkins.JenkinsException; import org.aravind.oss.jenkins.domain.Build; import org.aravind.oss.jenkins.domain.BuildCollection; import org.aravind.oss.kafka.connect.lib.SourceOffset; import org.aravind.oss.kafka.connect.lib.Partitions; import org.aravind.oss.kafka.connect.lib.SourcePartition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.kafka.common.utils.SystemTime; import org.apache.kafka.common.utils.Time; import static java.lang.String.valueOf; import static org.aravind.oss.kafka.connect.jenkins.JenkinsSourceConfig.*; import static org.aravind.oss.kafka.connect.jenkins.JenkinsSourceConfig.JENKINS_CONN_TIMEOUT_CONFIG; import static org.aravind.oss.kafka.connect.jenkins.Util.extractJobName; import static org.aravind.oss.kafka.connect.jenkins.Util.urlDecode; /** * @author Aravind R Yarram * @since 0.5.0 */ public class JenkinsSourceTask extends SourceTask { public static final String JOB_URLS = "job.urls"; public static final String JOB_NAME = "jobName"; public static final String BUILD_NUMBER = "buildNumber"; private static final Logger logger = LoggerFactory.getLogger(JenkinsSourceTask.class); private Time time; private long lastUpdate; private long pollIntervalInMillis; private static int totalJenkinsPulls = 1; private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); private final static Partitions partitions = new Partitions(JenkinsSourceTask.JOB_NAME); private Map<String, String> taskProps; private ObjectMapper mapper = new ObjectMapper(); private AtomicBoolean stop; private ReadYourWritesOffsetStorageAdapter storageAdapter; public JenkinsSourceTask() { this.time = new SystemTime(); } @Override public String version() { return Version.get(); } @Override public void start(Map<String, String> props) { logger.info("JenkinsSourceTask starting"); lastUpdate = 0; taskProps = props; pollIntervalInMillis = Long.parseLong(taskProps.get(JENKINS_POLL_INTERVAL_MS_CONFIG)); stop = new AtomicBoolean(false); } public Optional<SourceRecord> createSourceRecord(String jobUrl, Long lastSavedBuildNumber) { JenkinsClient client = null; try { if (getJenkinsUsername() != null && !getJenkinsUsername().isEmpty()) { client = new JenkinsClient(new URL(jobUrl + "api/json"), getJenkinsUsername(), getJenkinsPassword(), getJenkinsConnTimeout(), getJenkinsReadTimeout()); } else { client = new JenkinsClient(new URL(jobUrl + "api/json"), getJenkinsConnTimeout(), getJenkinsReadTimeout()); } } catch (JenkinsException je) { logger.error("Can't create URL object for Jenkins server at {}.", jobUrl, je); //TODO Silently log the error and ignore? What should we do? } catch (MalformedURLException e) { logger.error("Can't create URL object for Jenkins server at {}.", jobUrl, e); //TODO Silently log the error and ignore? What should we do? } Optional<String> resp = Optional.empty(); if (client != null) { try { logger.debug("GET job details for {}", jobUrl + "api/json"); resp = client.get(); } catch (JenkinsException e) { logger.warn("Can't do a GET to resource {}", jobUrl + "api/json", e); //TODO Silently log the error and ignore? What should we do? } if (resp.isPresent()) { logger.trace("Resp: {}", resp.get()); BuildCollection builds = null; //build SourceRecords try { builds = mapper.readValue(resp.get(), BuildCollection.class); } catch (IOException e) { logger.error("Error while parsing the Build JSON {} for {}", resp.get(), jobUrl + "api/json", e); } logger.trace("Builds are: {}", builds); if (builds != null) { SourcePartition partition = partitions.make(builds.getName()); Build lastBuild = builds.getLastBuild(); //Some jobs might not have any builds. TODO need to figure out how to represent these //And the lastBuild might have already been stored if (lastBuild != null && !lastBuild.getNumber().equals(lastSavedBuildNumber)) { Long offsetValue = lastBuild.getNumber(); logger.debug("Partition: {}, lastBuild: {}, lastSavedBuild: {}", partition.value, offsetValue, lastSavedBuildNumber); SourceOffset sourceOffset = SourceOffset.make(BUILD_NUMBER, offsetValue); //get Build details lastBuild.setConnTimeoutInMillis(getJenkinsConnTimeout()); lastBuild.setReadTimeoutInMillis(getJenkinsReadTimeout()); lastBuild.setUsername(getJenkinsUsername()); lastBuild.setPassword(getJenkinsPassword()); Optional<String> lastBuildDetails = lastBuild.getDetails(); if (lastBuildDetails.isPresent()) { //add build details JSON string as the value logger.debug("Create SourceRecord for {}", partition.value); SourceRecord record = new SourceRecord(partition.encoded, sourceOffset.encoded, taskProps.get(TOPIC_CONFIG), Schema.STRING_SCHEMA, partition.value, Schema.STRING_SCHEMA, lastBuildDetails.get()); storageAdapter.cache(partition, sourceOffset); return Optional.of(record); } else { logger.debug("Ignoring job details for {} as there are no builds for this Job. Not creating SourceRecord.", lastBuild.getBuildDetailsResource()); } } else { logger.debug("Not creating SourceRecord for {} because either the lastBuild details aren't available or it was already saved earlier", jobUrl + "api/json"); } } } else { //If no builds were found logger.debug("No builds were found for {}", jobUrl); } } return Optional.empty(); } @Override public List<SourceRecord> poll() throws InterruptedException { logger.debug("In poll()"); //Keep trying in a loop until stop() is called on this instance. //TODO use RxJava for this in future while (!stop.get()) { long now = time.milliseconds(); logger.trace("Now: {}", sdf.format(new Date(now))); long nextUpdate = 0; //Check if poll time had elapsed if (lastUpdate == 0) { logger.trace("First call after starting the connector. So Pulling the Jobs from Jenkins now."); nextUpdate = now; } else { nextUpdate = lastUpdate + pollIntervalInMillis; logger.trace("Next pull from Jenkins should happen at {} (approx).", nextUpdate); } long untilNext = nextUpdate - now; logger.debug("now: {}, nextUpdate: {}, untilNext: {}", sdf.format(new Date(now)), sdf.format(new Date(nextUpdate)), untilNext); if (untilNext > 0) { logger.info("Waiting {} ms before next pull", untilNext); time.sleep(untilNext); continue; } logger.debug("Total pulls from Jenkins so far: {}", totalJenkinsPulls); String jobUrls = taskProps.get(JOB_URLS); storageAdapter = new ReadYourWritesOffsetStorageAdapter(context.offsetStorageReader(), jobUrls, partitions); String[] jobUrlArray = jobUrls.split(","); List<SourceRecord> records = new ArrayList<>(); for (String jobUrl : jobUrlArray) { SourcePartition partition = partitions.make(urlDecode(extractJobName(jobUrl))); logger.trace("Get lastSavedOffset for: '{}' with partitionValue: {}", jobUrl, partition.value); Optional<SourceOffset> offset = storageAdapter.getOffset(partition); Long lastSavedBuildNumber = null; if (offset.isPresent()) { logger.debug("lastSavedOffset for '{}' is: {}", partition.value, offset.get()); lastSavedBuildNumber = (Long) offset.get().value; } else { logger.debug("lastSavedOffset not available for: {}", partition.value); } Optional<SourceRecord> sourceRecord = createSourceRecord(jobUrl, lastSavedBuildNumber); if (sourceRecord.isPresent()) records.add(sourceRecord.get()); } logger.info("Total SourceRecords created: {}. Returning these from poll()", records.size()); //Update the last updated time to now just before returning the call lastUpdate = time.milliseconds(); logger.debug("Setting the lastUpdate time to : {}", sdf.format(new Date(lastUpdate))); totalJenkinsPulls++; return records; } logger.debug("Returning null from poll(). This is because the runtime called shutdown."); //Only in case make shutdown. null indicates no data return null; } @Override public synchronized void stop() { logger.info("JenkinsSourceTask stopping"); if (stop != null) stop.set(true); } @Override public void initialize(SourceTaskContext context) { super.initialize(context); } private int getJenkinsReadTimeout() { return Integer.valueOf(taskProps.getOrDefault(JENKINS_READ_TIMEOUT_CONFIG, valueOf(JENKINS_READ_TIMEOUT_DEFAULT))); } private int getJenkinsConnTimeout() { return Integer.valueOf(taskProps.getOrDefault(JENKINS_CONN_TIMEOUT_CONFIG, valueOf(JENKINS_CONN_TIMEOUT_DEFAULT))); } private String getJenkinsUsername() { return String.valueOf(taskProps.getOrDefault(JENKINS_USERNAME_CONFIG, "")); } private String getJenkinsPassword() { return String.valueOf(taskProps.getOrDefault(JENKINS_PASSWORD_OR_API_TOKEN_CONFIG, "")); } }