diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4c25ef2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +Copyright (c) 2012, Alexander Kurdyukov +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md index d34f501..758f5a3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,17 @@ bamboo-traffic-light-plugin =========================== -Atlassian Bamboo plugin for toy traffic light \ No newline at end of file +Atlassian Bamboo plugin for toy traffic light. + +Presents simple TCP server for managing toy traffic light. Listening port is not configurable, it's 9090. + +Settings +-------- +User may set traffic light program (text message) for every plan. See sections 'Pre Build Traffic Light Program' and 'Post Build Traffic Light Program' in job configuration tab 'Miscellaneous'. + +Protocol +-------- +After connecting to port 9090 user must send plan name followed by '\n'. Server disconnects client with unknown plan names. + +When plan build starts all listening subscribers receive pre-build program followed with '\n'. +When plan build finishes (successful or failure) all listening subscribers receive appropriate post-build program followed with '\n'. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0adb7a7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,71 @@ + + + + + 4.0.0 + com.fvendor + bamboo-traffic-light-plugin + 1.0-SNAPSHOT + + + FVendor Inc. + http://www.fvendor.com/ + + + bamboo-traffic-light-plugin + Traffic light control plugin. + atlassian-plugin + + + 4.2.1 + 3.2.2 + 4.0 + + + + + com.atlassian.bamboo + atlassian-bamboo-web + ${bamboo.version} + provided + + + + org.xsocket + xSocket + 2.8.15 + + + + junit + junit + 4.6 + test + + + + + + + com.atlassian.maven.plugins + maven-bamboo-plugin + ${amps.version} + true + + ${bamboo.version} + ${bamboo.data.version} + + + + + maven-compiler-plugin + + 1.6 + 1.6 + + + + + diff --git a/src/main/java/bamboo/traflight/ExampleTask.java b/src/main/java/bamboo/traflight/ExampleTask.java new file mode 100644 index 0000000..724d32f --- /dev/null +++ b/src/main/java/bamboo/traflight/ExampleTask.java @@ -0,0 +1,24 @@ +package bamboo.traflight; + +import com.atlassian.bamboo.build.logger.BuildLogger; +import com.atlassian.bamboo.task.TaskContext; +import com.atlassian.bamboo.task.TaskException; +import com.atlassian.bamboo.task.TaskResult; +import com.atlassian.bamboo.task.TaskResultBuilder; +import com.atlassian.bamboo.task.TaskType; +import org.jetbrains.annotations.NotNull; + +public class ExampleTask implements TaskType +{ + @NotNull + @java.lang.Override + public TaskResult execute(@NotNull final TaskContext taskContext) throws TaskException + { + final BuildLogger buildLogger = taskContext.getBuildLogger(); + + final String toSay = taskContext.getConfigurationMap().get("say"); + buildLogger.addBuildLogEntry(toSay); + + return TaskResultBuilder.create(taskContext).success().build(); + } +} \ No newline at end of file diff --git a/src/main/java/bamboo/traflight/ExampleTaskConfigurator.java b/src/main/java/bamboo/traflight/ExampleTaskConfigurator.java new file mode 100644 index 0000000..f8fa1e7 --- /dev/null +++ b/src/main/java/bamboo/traflight/ExampleTaskConfigurator.java @@ -0,0 +1,67 @@ +package bamboo.traflight; + +import com.atlassian.bamboo.collections.ActionParametersMap; +import com.atlassian.bamboo.task.AbstractTaskConfigurator; +import com.atlassian.bamboo.task.TaskDefinition; +import com.atlassian.bamboo.utils.error.ErrorCollection; +import com.opensymphony.xwork.TextProvider; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public class ExampleTaskConfigurator extends AbstractTaskConfigurator +{ + private TextProvider textProvider; + + @NotNull + @Override + public Map generateTaskConfigMap(@NotNull final ActionParametersMap params, @Nullable final TaskDefinition previousTaskDefinition) + { + final Map config = super.generateTaskConfigMap(params, previousTaskDefinition); + config.put("say", params.getString("say")); + return config; + } + + @Override + public void populateContextForCreate(@NotNull final Map context) + { + super.populateContextForCreate(context); + + context.put("say", "Hello, World!"); + } + + @Override + public void populateContextForEdit(@NotNull final Map context, @NotNull final TaskDefinition taskDefinition) + { + super.populateContextForEdit(context, taskDefinition); + + context.put("say", taskDefinition.getConfiguration().get("say")); + } + + @Override + public void populateContextForView(@NotNull final Map context, @NotNull final TaskDefinition taskDefinition) + { + super.populateContextForView(context, taskDefinition); + context.put("say", taskDefinition.getConfiguration().get("say")); + } + + @Override + public void validate(@NotNull final ActionParametersMap params, @NotNull final ErrorCollection errorCollection) + { + super.validate(params, errorCollection); + + final String sayValue = params.getString("say"); + if (StringUtils.isEmpty(sayValue)) + { + errorCollection.addError("say", textProvider.getText("com.fvendor.say.error")); + } + } + + public void setTextProvider(final TextProvider textProvider) + { + this.textProvider = textProvider; + } +} diff --git a/src/main/java/com/fvendor/bamboo/traflight/PostBuildCompletedAction.java b/src/main/java/com/fvendor/bamboo/traflight/PostBuildCompletedAction.java new file mode 100644 index 0000000..566f6e7 --- /dev/null +++ b/src/main/java/com/fvendor/bamboo/traflight/PostBuildCompletedAction.java @@ -0,0 +1,79 @@ +package com.fvendor.bamboo.traflight; + +import com.atlassian.bamboo.build.BuildLoggerManager; +import com.atlassian.bamboo.build.CustomPostBuildCompletedAction; +import com.atlassian.bamboo.builder.BuildState; +import com.atlassian.bamboo.v2.build.BaseConfigurablePlugin; +import com.atlassian.bamboo.v2.build.BuildContext; +import org.apache.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +/** + * Post build completed action sending program to traffic light + */ +public class PostBuildCompletedAction extends BaseConfigurablePlugin implements CustomPostBuildCompletedAction { + private static final Logger log = Logger.getLogger(PostBuildCompletedAction.class); + + private static final String PRE_ENABLED_KEY = "custom.traflight.post.program.enabled"; + private static final String PRE_PROGRAM_SUCCESS_KEY = "custom.traflight.post.program.success"; + private static final String PRE_PROGRAM_FAIL_KEY = "custom.traflight.post.program.fail"; + + private BuildContext buildContext; + private BuildLoggerManager buildLoggerManager; + private TrafficLightService trafficLightService; + + @Override + public void init(@NotNull BuildContext buildContext) { + this.buildContext = buildContext; + } + + @NotNull + @Override + public BuildContext call() throws InterruptedException, Exception { + final Map customConfiguration = buildContext.getBuildDefinition().getCustomConfiguration(); + + if (!"true".equals(customConfiguration.get(PRE_ENABLED_KEY))) { + log.info("Post traffic light disabled, skipping"); + return buildContext; + } + + BuildState state = buildContext.getBuildResult().getBuildState(); + if (state.equals(BuildState.UNKNOWN)) { + if (0 < buildContext.getBuildResult().getBuildReturnCode()) { + state = BuildState.FAILED; + } else { + state = BuildState.SUCCESS; + } + } + String program; + if (BuildState.SUCCESS.equals(state)) { + program = (String) customConfiguration.get(PRE_PROGRAM_SUCCESS_KEY); + } else { + program = (String) customConfiguration.get(PRE_PROGRAM_FAIL_KEY); + } + + log.info("Post traffic light enabled, setting program " + program); + buildLoggerManager.getBuildLogger(buildContext.getPlanResultKey()).addBuildLogEntry( + "setting post traffic light program " + program); + sendProgram(buildContext, program); + + return buildContext; + } + + private void sendProgram(BuildContext context, String program) { + if (context.getParentBuildContext() != null) { + sendProgram(context.getParentBuildContext(), program); + } + trafficLightService.setProgram(context.getPlanKey(), program); + } + + public void setBuildLoggerManager(BuildLoggerManager buildLoggerManager) { + this.buildLoggerManager = buildLoggerManager; + } + + public void setTrafficLightService(TrafficLightService trafficLightService) { + this.trafficLightService = trafficLightService; + } +} diff --git a/src/main/java/com/fvendor/bamboo/traflight/PreBuildAction.java b/src/main/java/com/fvendor/bamboo/traflight/PreBuildAction.java new file mode 100644 index 0000000..4851486 --- /dev/null +++ b/src/main/java/com/fvendor/bamboo/traflight/PreBuildAction.java @@ -0,0 +1,63 @@ +package com.fvendor.bamboo.traflight; + +import com.atlassian.bamboo.build.BuildLoggerManager; +import com.atlassian.bamboo.build.CustomPreBuildAction; +import com.atlassian.bamboo.v2.build.BaseConfigurableBuildPlugin; +import com.atlassian.bamboo.v2.build.BuildContext; +import org.apache.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +/** + * Pre-build action sends 'build started' command + */ +public class PreBuildAction extends BaseConfigurableBuildPlugin implements CustomPreBuildAction { + private static final Logger log = Logger.getLogger(PreBuildAction.class); + + private static final String PRE_ENABLED_KEY = "custom.traflight.pre.program.enabled"; + private static final String PRE_PROGRAM_KEY = "custom.traflight.pre.program"; + + private BuildContext buildContext; + private BuildLoggerManager buildLoggerManager; + private TrafficLightService trafficLightService; + + @Override + public void init(@NotNull BuildContext buildContext) { + this.buildContext = buildContext; + } + + @NotNull + @Override + public BuildContext call() throws Exception { + final Map customConfiguration = buildContext.getBuildDefinition().getCustomConfiguration(); + + log.info("Pre traffic light enabled: " + customConfiguration.get(PRE_ENABLED_KEY)); + if (!"true".equals(customConfiguration.get(PRE_ENABLED_KEY))) { + log.info("Pre traffic light disabled, skipping"); + return buildContext; + } + + String program = (String) customConfiguration.get(PRE_PROGRAM_KEY); + log.info("Pre traffic light enabled, setting program " + program); + buildLoggerManager.getBuildLogger(buildContext.getPlanResultKey()).addBuildLogEntry( + "setting pre traffic light program " + program); + sendProgram(buildContext, program); + return buildContext; + } + + private void sendProgram(BuildContext context, String program) { + if (context.getParentBuildContext() != null) { + sendProgram(context.getParentBuildContext(), program); + } + trafficLightService.setProgram(context.getPlanKey(), program); + } + + public void setBuildLoggerManager(BuildLoggerManager buildLoggerManager) { + this.buildLoggerManager = buildLoggerManager; + } + + public void setTrafficLightService(TrafficLightService trafficLightService) { + this.trafficLightService = trafficLightService; + } +} diff --git a/src/main/java/com/fvendor/bamboo/traflight/TcpTrafficLightService.java b/src/main/java/com/fvendor/bamboo/traflight/TcpTrafficLightService.java new file mode 100644 index 0000000..d6e2838 --- /dev/null +++ b/src/main/java/com/fvendor/bamboo/traflight/TcpTrafficLightService.java @@ -0,0 +1,90 @@ +package com.fvendor.bamboo.traflight; + +import com.atlassian.bamboo.plan.PlanManager; +import com.atlassian.bamboo.plan.TopLevelPlan; +import com.atlassian.bamboo.security.BambooPermissionManager; +import com.atlassian.spring.container.ContainerManager; +import org.acegisecurity.context.SecurityContextHolder; +import org.apache.log4j.Logger; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.xsocket.MaxReadSizeExceededException; +import org.xsocket.connection.*; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.channels.ClosedChannelException; +import java.util.HashMap; +import java.util.Map; + +/** + * xSocket TCP server implementing TrafficLightSource + */ +public class TcpTrafficLightService implements TrafficLightService, InitializingBean, DisposableBean, IDataHandler { + private static final Logger log = Logger.getLogger(TcpTrafficLightService.class); + + private int serverPort = 9090; + private Map programs = new HashMap(); + private Server server; + + @Override + public synchronized void setProgram(String buildPlan, String program) { + programs.put(buildPlan, program); + int found = 0; + for(INonBlockingConnection conn: server.getOpenConnections()) { + String login = (String) conn.getAttachment(); + if (!buildPlan.equals(login)) { + continue; + } + try { + conn.write(program); + conn.write('\n'); + conn.flush(); + found++; + } catch (IOException e) { + log.error("Error sending program to client", e); + } + } + log.info("Program " + program + " set to " + found + " connected lights for plan " + buildPlan); + } + + @Override + public void destroy() throws Exception { + server.close(); + log.info("Traffic light server stopped"); + } + + @Override + public void afterPropertiesSet() throws Exception { + SecurityContextHolder.getContext().setAuthentication(BambooPermissionManager.SYSTEM_AUTHORITY); + PlanManager planManager = (PlanManager) ContainerManager.getComponent("planManager"); + + for (TopLevelPlan plan : planManager.getAllPlans()) { + programs.put(plan.getPlanKey().getKey(), ""); + log.info("Default program added for plan " + plan.getPlanKey().getKey()); + } + + server = new Server(serverPort, this); + server.start(); + log.info("Started traffic light server on port " + serverPort); + } + + @Override + public boolean onData(INonBlockingConnection conn) throws IOException, BufferUnderflowException, ClosedChannelException, MaxReadSizeExceededException { + // handle simplistic auth + String login = conn.readStringByDelimiter("\n").trim(); + log.info("Got login " + login); + if (!programs.containsKey(login)) { + log.warn("Login is invalid, closing connection"); + conn.close(); // close invalid connection + } + conn.setAttachment(login); + String program = programs.get(login); + if (program != null && !program.trim().equals("")) { + conn.write(program); + conn.write('\n'); + conn.flush(); + } + return true; + } +} diff --git a/src/main/java/com/fvendor/bamboo/traflight/TrafficLightService.java b/src/main/java/com/fvendor/bamboo/traflight/TrafficLightService.java new file mode 100644 index 0000000..f07af98 --- /dev/null +++ b/src/main/java/com/fvendor/bamboo/traflight/TrafficLightService.java @@ -0,0 +1,13 @@ +package com.fvendor.bamboo.traflight; + +/** + * Public interface for traffic light interaction service + */ +public interface TrafficLightService { + /** + * Set program for traffic light + * + * @param program program source + */ + void setProgram(String buildPlan, String program); +} diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml new file mode 100644 index 0000000..ee7e4ef --- /dev/null +++ b/src/main/resources/atlassian-plugin.xml @@ -0,0 +1,26 @@ + + + ${project.description} + ${project.version} + + + + + Provides TCP server for traffic light. + com.fvendor.bamboo.traflight.TrafficLightService + + + + A custom action that sets starting position for connected traffic light + + + + + + A custom action that sets finish position for connected traffic light + + + + diff --git a/src/main/resources/english.properties b/src/main/resources/english.properties new file mode 100644 index 0000000..157c6db --- /dev/null +++ b/src/main/resources/english.properties @@ -0,0 +1,3 @@ +com.fvendor.say = Say +com.fvendor.say.description = What should Bamboo print to the log? +com.fvendor.say.error = You did not configure Bamboo with anything to say. \ No newline at end of file diff --git a/src/main/resources/templates/postBuildAction/traflightFinishEdit.ftl b/src/main/resources/templates/postBuildAction/traflightFinishEdit.ftl new file mode 100644 index 0000000..9cc6c28 --- /dev/null +++ b/src/main/resources/templates/postBuildAction/traflightFinishEdit.ftl @@ -0,0 +1,5 @@ +[@ui.bambooSection title='Post Build Traffic Light Program'] + [@ww.textfield name='custom.traflight.post.program.success' label='Success Program' description='Program to set when build finishes successfully' cssClass="long-field" /] + [@ww.textfield name='custom.traflight.post.program.fail' label='Fail Program' description='Program to set when build fails' cssClass="long-field" /] + [@ww.checkbox name='custom.traflight.post.program.enabled' label='Enabled?' toggle='true' description='Enables/disables traffic light signaling' /] +[/@ui.bambooSection] \ No newline at end of file diff --git a/src/main/resources/templates/postBuildAction/traflightFinishView.ftl b/src/main/resources/templates/postBuildAction/traflightFinishView.ftl new file mode 100644 index 0000000..89f62d7 --- /dev/null +++ b/src/main/resources/templates/postBuildAction/traflightFinishView.ftl @@ -0,0 +1,13 @@ +[#if plan?? && plan.buildDefinition?? && plan.buildDefinition.customConfiguration.get('custom.traflight.post.program.enabled')?? ] + [@ui.bambooInfoDisplay titleKey='Post Build Traffic Light Program' float=false height='80px'] + + [@ww.label label='Success Program' ] + [@ww.param name='value']${plan.buildDefinition.customConfiguration.get('custom.traflight.post.program.success')!}[/@ww.param] + [/@ww.label] + + [@ww.label label='Fail Program' ] + [@ww.param name='value']${plan.buildDefinition.customConfiguration.get('custom.traflight.post.program.fail')!}[/@ww.param] + [/@ww.label] + + [/@ui.bambooInfoDisplay] +[/#if] \ No newline at end of file diff --git a/src/main/resources/templates/preBuildAction/traflightStartEdit.ftl b/src/main/resources/templates/preBuildAction/traflightStartEdit.ftl new file mode 100644 index 0000000..77f7661 --- /dev/null +++ b/src/main/resources/templates/preBuildAction/traflightStartEdit.ftl @@ -0,0 +1,4 @@ +[@ui.bambooSection title='Pre Build Traffic Light Program'] + [@ww.textfield name='custom.traflight.pre.program' label='Program' description='Program to set when build starts' cssClass="long-field" /] + [@ww.checkbox name='custom.traflight.pre.program.enabled' label='Enabled?' toggle='true' description='Enables/disables traffic light signaling' /] +[/@ui.bambooSection] \ No newline at end of file diff --git a/src/main/resources/templates/preBuildAction/traflightStartView.ftl b/src/main/resources/templates/preBuildAction/traflightStartView.ftl new file mode 100644 index 0000000..4f7975c --- /dev/null +++ b/src/main/resources/templates/preBuildAction/traflightStartView.ftl @@ -0,0 +1,9 @@ +[#if plan?? && plan.buildDefinition?? && plan.buildDefinition.customConfiguration.get('custom.traflight.pre.program.enabled')?? ] + [@ui.bambooInfoDisplay titleKey='Pre Build Traffic Light Program' float=false height='80px'] + + [@ww.label label='Program' ] + [@ww.param name='value']${plan.buildDefinition.customConfiguration.get('custom.traflight.pre.program')!}[/@ww.param] + [/@ww.label] + + [/@ui.bambooInfoDisplay] +[/#if] \ No newline at end of file diff --git a/src/test/java/it/IntegrationTestMyPlugin.java b/src/test/java/it/IntegrationTestMyPlugin.java new file mode 100644 index 0000000..4a1af31 --- /dev/null +++ b/src/test/java/it/IntegrationTestMyPlugin.java @@ -0,0 +1,10 @@ +package it; + +import junit.framework.TestCase; + +public class IntegrationTestMyPlugin extends TestCase +{ + public void testSomething() + { + } +} diff --git a/src/test/resources/TEST_RESOURCES_README.TXT b/src/test/resources/TEST_RESOURCES_README.TXT new file mode 100644 index 0000000..18ac9db --- /dev/null +++ b/src/test/resources/TEST_RESOURCES_README.TXT @@ -0,0 +1,3 @@ +Create any of the test resources you might need in this directory. + +Please remove this file before releasing your plugin.