Last active
June 16, 2016 21:56
-
-
Save xlcommunity/93b63a414df15798fd2d to your computer and use it in GitHub Desktop.
XL Release "on failure handler" example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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