Skip to content

Instantly share code, notes, and snippets.

@xlcommunity
Last active June 16, 2016 21:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xlcommunity/93b63a414df15798fd2d to your computer and use it in GitHub Desktop.
Save xlcommunity/93b63a414df15798fd2d to your computer and use it in GitHub Desktop.
XL Release "on failure handler" example
# THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS
# FOR A PARTICULAR PURPOSE. THIS CODE AND INFORMATION ARE NOT SUPPORTED BY XEBIALABS.
from com.xebialabs.deployit.exception import NotFoundException
from com.xebialabs.xlrelease.api.v1.forms import Comment, Variable
from com.xebialabs.xlrelease.domain import Task
RELEASE_ID_PARAM = 'releaseId'
ON_FAILURE_USER_PARAM = 'onFailureUser'
MANUAL_TASK_TYPE = 'xlrelease.Task'
BOOLEAN_VARIABLE_TYPE = 'xlrelease.BooleanVariable'
RELEASE_FAILED_VARIABLE_NAME = 'releaseFailed'
ON_FAILURE_PHASE_NAME = 'onFailure'
IS_ON_FAILURE_PHASE = lambda phase: phase.getTitle() == ON_FAILURE_PHASE_NAME
IS_PARALLEL_GROUP = lambda task: task.getType() == 'xlrelease.ParallelGroup'
def has_onFailure_phase(release):
for phase in release.getPhases():
if IS_ON_FAILURE_PHASE(phase):
return True
return False
def has_failed_phase(release):
for phase in release.getPhases():
if phase.isFailed():
return True
return False
def add_placeholder_task(release, assignedUser):
for phase in release.getPhases():
if phase.isFailed():
tasks = phase.getTasks()
numTasks = len(tasks)
for i in range(numTasks):
if tasks[i].isFailed():
logger.trace('Adding manual task in phase {} at position {}', phase.getTitle(), i+1)
placeholderTask = Task.fromType(MANUAL_TASK_TYPE)
placeholderTask.setTitle('Skip to Fallback')
placeholderTask.setDescription('Automatically added by onFailure handler')
placeholderTask.setOwner(assignedUser)
if i < numTasks-1:
savedPlaceholderTask = phaseApi.addTask(phase.getId(), placeholderTask, i+1)
else:
# add at the end of the phase
savedPlaceholderTask = phaseApi.addTask(phase.getId(), placeholderTask)
return savedPlaceholderTask.getId()
def skip_task(taskId, assignedUser, comment):
taskApi.assignTask(taskId, assignedUser)
taskApi.skipTask(taskId, comment)
def new_boolean_var(name, value, required=False, label=None, description=None):
# setting a dummy value since we can only pass a string here
variable = Variable(name, None, required)
variable.setType(BOOLEAN_VARIABLE_TYPE)
variable.setValue(value)
if label:
variable.setLabel(label)
if description:
variable.setDescription(description)
return variable
if not RELEASE_ID_PARAM in request.query:
responseMsg = "Required parameter '%s' missing. Doing nothing" % (RELEASE_ID_PARAM)
elif not ON_FAILURE_USER_PARAM in request.query:
responseMsg = "Required parameter '%s' missing. Doing nothing" % (ON_FAILURE_USER_PARAM)
else:
releaseId = request.query[RELEASE_ID_PARAM]
if not releaseId.startswith('Applications/'):
releaseId = "Applications/%s" % (releaseId)
onFailureUsername = request.query[ON_FAILURE_USER_PARAM]
logger.info('Invoking onFailure handler for releaseId {} with tracking user {}', releaseId, onFailureUsername)
try:
release = releaseApi.getRelease(releaseId)
except NotFoundException:
release = None
if not release:
logger.debug("No release '{}' found. Doing nothing", releaseId)
responseMsg = "Release '%s' not found. Doing nothing" % (releaseId)
elif not has_onFailure_phase(release):
logger.debug('No phase {} found. Doing nothing', ON_FAILURE_PHASE_NAME)
responseMsg = "onFailure handler does not apply to this release as there is no phase named '%s'. Doing nothing" % (ON_FAILURE_PHASE_NAME)
elif not has_failed_phase(release):
logger.debug('No failed phase found. Doing nothing')
responseMsg = 'onFailure handler does not apply to this release as there is no failed phase. Doing nothing'
elif RELEASE_FAILED_VARIABLE_NAME in release.getVariablesByKeys():
logger.debug('Variable "{}" already present. Doing nothing', RELEASE_FAILED_VARIABLE_NAME)
responseMsg = 'onFailure handler already invoked for this release. Doing nothing'
else:
logger.debug('Adding "{}" variable', RELEASE_FAILED_VARIABLE_NAME)
releaseFailedVar = new_boolean_var(RELEASE_FAILED_VARIABLE_NAME, True, False, 'Has this release failed?', 'Automatically set by onFailure handler')
releaseApi.createVariable(release.getId(), releaseFailedVar)
# add a manual placeholder task immediately after the first failure
# so we can activate the release again
logger.debug('Adding placeholder task')
placeholderTaskId = add_placeholder_task(release, onFailureUsername)
logger.debug('Skipping failed tasks')
skipComment = Comment('Skipped by onFailure handler')
for task in release.getAllTasks():
if task.isFailed() and not IS_PARALLEL_GROUP(task):
logger.trace('Skipping failed task {}', task.getId())
taskApi.skipTask(task.getId(), skipComment)
logger.debug('Skipping planned tasks')
for phase in release.getPhases():
if not IS_ON_FAILURE_PHASE(phase):
for task in phase.getAllTasks():
# leave the placeholder task running
if (task.isPlanned() or task.isInProgress()) and not IS_PARALLEL_GROUP(task) and task.getId() != placeholderTaskId:
logger.trace('Skipping planned or in progress task {}', task.getId())
skip_task(task.getId(), onFailureUsername, skipComment)
logger.debug('Skipping placeholder task')
skip_task(placeholderTaskId, onFailureUsername, skipComment)
responseMsg = "Successfully executed onFailure handler for release '%s'" % (releaseId)'
response.entity = { 'message': responseMsg }
/*
* THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
* EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. THIS
* CODE AND INFORMATION ARE NOT SUPPORTED BY XEBIALABS.
*/
package ext.deployit.onfailurehandler;
import static java.util.concurrent.TimeUnit.SECONDS;
import static com.google.common.base.Strings.isNullOrEmpty;
import java.net.URISyntaxException;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.xebialabs.deployit.ServerConfiguration;
import com.xebialabs.deployit.engine.spi.event.*;
import com.xebialabs.deployit.plugin.api.udm.ConfigurationItem;
import com.xebialabs.deployit.plugin.api.reflect.Type;
import com.xebialabs.xlrelease.api.XLReleaseServiceHolder;
import com.xebialabs.xlrelease.domain.Release;
import com.xebialabs.xlrelease.domain.status.ReleaseStatus;
import com.xebialabs.xlrelease.domain.variables.Variable;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import nl.javadude.t2bus.Subscribe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@DeployitEventListener
public class OnReleaseFailureEventListener {
private static final Logger logger = LoggerFactory.getLogger(OnReleaseFailureEventListener.class);
private static final Type RELEASE_TYPE = Type.valueOf("xlrelease.Release");
private static final String ENDPOINT_VARIABLE_NAME =
"global.onFailureHandlerPath";
private static final String ENDPOINT_USER = "onFailure_user";
private static final String ENDPOINT_USER_PASSWORD_PROPERTY =
"onFailureHandler.password";
private static final String ENDPOINT_SCHEME = "http";
private static final String ENDPOINT_HOST = "localhost";
private static final int ENDPOINT_PORT = 5516;
/*
* The onFailure handler already contains a mechanism to try to ensure it
* isn't executed multiple times for the same release. However, there is
* a small window during which the endpoint cannot detect whether it has
* been called for a certain release before.
*
* Since we seem to be getting two events for the same task failure in
* rapid succession, this cache is intended to try to prevent duplicate
* calls for the *same* failure before the endpoint has had a chance to
* 'tag' the release.
*/
private static final Cache<String, Boolean> FAILED_RELEASES_SEEN =
CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, SECONDS)
.<String, Boolean>build();
private static final ExecutorService EXECUTOR_SERVICE =
Executors.newSingleThreadExecutor();
private static final String UNINITIALIZED = "uninitialized";
private static final CloseableHttpClient HTTP_CLIENT;
private static final AuthCache PREEMPTIVE_AUTH_CACHE;
static {
HttpHost target =
new HttpHost(ENDPOINT_HOST, ENDPOINT_PORT, ENDPOINT_SCHEME);
String endpointUserPassword = ServerConfiguration.getInstance()
.getCustomPassword(ENDPOINT_USER_PASSWORD_PROPERTY);
if (isNullOrEmpty(endpointUserPassword)) {
logger.warn("No configuration property '{}' found in xl-release-server.conf. Calls to the onFailure handler will fail to authenticate",
ENDPOINT_USER_PASSWORD_PROPERTY);
}
CredentialsProvider credentials = new BasicCredentialsProvider();
credentials.setCredentials(
new AuthScope(target.getHostName(), target.getPort()),
new UsernamePasswordCredentials(ENDPOINT_USER,
endpointUserPassword));
HTTP_CLIENT = HttpClients.custom()
.setDefaultCredentialsProvider(credentials)
.build();
PREEMPTIVE_AUTH_CACHE = new BasicAuthCache();
PREEMPTIVE_AUTH_CACHE.put(target, new BasicScheme());
}
private final AtomicReference<String> handlerEndpoint =
new AtomicReference<String>(UNINITIALIZED);
private String getHandlerEndpoint() {
String value = handlerEndpoint.get();
// don't worry about potential double calls to the API
if (value != UNINITIALIZED) {
return value;
}
for (Variable variable : XLReleaseServiceHolder
.getConfigurationApi().getGlobalVariables()) {
if (variable.getKey().equalsIgnoreCase(ENDPOINT_VARIABLE_NAME)) {
value = variable.getValueAsString();
handlerEndpoint.compareAndSet(UNINITIALIZED, value);
return value;
}
}
return UNINITIALIZED;
}
@Subscribe
public void receiveCisUpdated(CisUpdatedEvent event) {
for (ConfigurationItem ci : event.getCis()) {
if (ci.getType().instanceOf(RELEASE_TYPE)) {
Release release = (Release) ci;
if (release.getStatus() == ReleaseStatus.FAILED) {
// see comment where FAILED_RELEASES_SEEN is declared
if (FAILED_RELEASES_SEEN.getIfPresent(release.getId()) != null) {
logger.debug("Release '{}' already seen. Doing nothing", release.getId());
} else {
FAILED_RELEASES_SEEN.put(release.getId(), true);
/*
* Needs to be called from a different thread to
* allow this event handler to complete. Otherwise,
* the release cannot be modified by the handler.
*/
logger.debug("Submitting runnable to invoke onFailure handler for release '{}'", release.getId());
EXECUTOR_SERVICE.submit(new Runnable() {
public void run() {
try {
invokeOnFailureHandler(release);
} catch (IOException | URISyntaxException exception) {
logger.error("Exception trying to invoke onFailure callback: {}", exception);
}
}
});
}
}
}
}
}
private void invokeOnFailureHandler(Release release) throws IOException, URISyntaxException {
String handlerEndpoint = getHandlerEndpoint();
// sentinel object so OK to use ==
if (handlerEndpoint == UNINITIALIZED) {
logger.error("Global variable '{}' not found! Doing nothing", ENDPOINT_VARIABLE_NAME);
return;
}
URIBuilder requestUri = new URIBuilder()
.setScheme(ENDPOINT_SCHEME)
.setHost(ENDPOINT_HOST)
.setPort(ENDPOINT_PORT)
.setPath(getHandlerEndpoint())
.addParameter("releaseId", release.getId())
.addParameter("onFailureUser", ENDPOINT_USER);
HttpGet request = new HttpGet(requestUri.build());
// without this, Apache HC will only send auth *after* a failure
HttpClientContext authenticatingContext = HttpClientContext.create();
authenticatingContext.setAuthCache(PREEMPTIVE_AUTH_CACHE);
logger.debug("About to execute callback to {}", request);
CloseableHttpResponse response =
HTTP_CLIENT.execute(request, authenticatingContext);
try {
logger.info("Response line from request: {}",
response.getStatusLine());
logger.debug("Response body: {}",
EntityUtils.toString(response.getEntity()));
} finally {
response.close();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment