Skip to content

Commit

Permalink
Adding build trigger for new machine added to octopus
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-richardson committed Mar 10, 2016
1 parent 16a3a38 commit 4376c5e
Show file tree
Hide file tree
Showing 38 changed files with 1,806 additions and 3 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A TeamCity plugin that polls Octopus Deploy, and triggers a TeamCity build when:
- [x] a deployment to an environment is complete
- [x] a successful deployment to an environment is complete
- [x] a release is created
- [ ] a new tentacle is added
- [x] a new tentacle is added

This is very much a work in progress, but feel free to give it a go, and let me know if you face any issues.
Constructive criticism received gratefully - this is my first real java project, and there's a lot I'm unaware of in that ecosystem.
Expand All @@ -19,7 +19,6 @@ See [TeamCity documentation](https://confluence.jetbrains.com/display/TCD9/Insta

# Outstanding items

- overall test coverage isn't great
- test older versions of Octopus
- pass details of trigger item (ie, release name), to build. At the moment, you need to parse a configuration parameter `teamcity.build.triggeredBy`, which is designed to be a human readable rather than machine readable.
- improve logging
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.mjrichardson.teamCity.buildTriggers;

import com.mjrichardson.teamCity.buildTriggers.MachineAdded.Machine;
import com.mjrichardson.teamCity.buildTriggers.MachineAdded.Machines;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

import java.util.List;
import java.util.Map;

public class ApiMachinesResponse {
public Machines machines;
public String nextLink;

public ApiMachinesResponse(String machinesResponse) throws ParseException {
JSONParser parser = new JSONParser();
Map response = (Map) parser.parse(machinesResponse);

machines = new Machines();

List items = (List) response.get("Items");
for (Object item : items) {
machines.add(Machine.Parse((Map) item));
}

Object nextPage = ((Map) response.get("Links")).get("Page.Next");
if (nextPage != null)
nextLink = nextPage.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@
public class ApiRootResponse {
public final String deploymentsApiLink;
public final String progressionApiLink; //todo: not used outside of tests
public final String progressionApiLink;
public final String projectsApiLink;
public final String machinesApiLink;

private static final Logger LOG = Logger.getInstance(ApiRootResponse.class.getName());

public ApiRootResponse(String apiResponse) throws ParseException {
deploymentsApiLink = parseLink(apiResponse, "Deployments", "/api/deployments");
progressionApiLink = parseLink(apiResponse, "Progression", "/api/progression");
projectsApiLink = parseLink(apiResponse, "Projects", "/api/projects");
machinesApiLink = parseLink(apiResponse, "Machines", "/api/machines");
}

private String parseLink(String apiResponse, String linkName, String defaultResponse) throws ParseException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.mjrichardson.teamCity.buildTriggers.MachineAdded;

import java.util.Map;

public class Machine implements Comparable<Machine> {
public final String id;
public final String name;

public Machine(String MachineId, String version) {
this.id = MachineId;
this.name = version;
}

@Override
public String toString() {
return id + ";" + name;
}

public static Machine Parse(Map item) {
String id = item.get("Id").toString();
String name = item.get("Name").toString();

return new Machine(id, name);
}

public static Machine Parse(String pair) {
if (pair == null || pair == "") {
return new NullMachine();
}
final Integer DONT_REMOVE_EMPTY_VALUES = -1;
final String[] split = pair.split(";", DONT_REMOVE_EMPTY_VALUES);
final String MachineId = split[0];
final String version = split[1];

Machine result = new Machine(MachineId, version);
if (result.equals(new NullMachine()))
return new NullMachine();
return result;
}

@Override
public int compareTo(Machine o) {
Integer thisId = Integer.parseInt(id.split("-")[1]);
Integer otherId = Integer.parseInt(o.id.split("-")[1]);
return thisId.compareTo(otherId);
}

@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if (obj.getClass() != Machine.class && obj.getClass() != NullMachine.class)
return false;
return toString().equals(obj.toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.mjrichardson.teamCity.buildTriggers.MachineAdded;

import jetbrains.buildServer.buildTriggers.BuildTriggerDescriptor;
import jetbrains.buildServer.buildTriggers.BuildTriggerException;
import jetbrains.buildServer.buildTriggers.async.*;
import org.jetbrains.annotations.NotNull;

import java.util.Map;

import static com.mjrichardson.teamCity.buildTriggers.OctopusBuildTriggerUtil.OCTOPUS_URL;

class MachineAddedAsyncBuildTrigger implements AsyncBuildTrigger<MachineAddedSpec> {
private final String displayName;
private final int pollIntervalInSeconds;

public MachineAddedAsyncBuildTrigger(String displayName, int pollIntervalInSeconds) {
this.displayName = displayName;
this.pollIntervalInSeconds = pollIntervalInSeconds;
}

@NotNull
public BuildTriggerException makeTriggerException(@NotNull Throwable throwable) {
throw new BuildTriggerException(displayName + " failed with error: " + throwable.getMessage(), throwable);
}

@NotNull
public String getRequestorString(@NotNull MachineAddedSpec machineAddedSpec) {
return machineAddedSpec.getRequestorString();
}

public int getPollInterval(@NotNull AsyncTriggerParameters parameters) {
return pollIntervalInSeconds;
}

@NotNull
public CheckJob<MachineAddedSpec> createJob(@NotNull final AsyncTriggerParameters asyncTriggerParameters) throws CheckJobCreationException {
return new MachineAddedCheckJob(displayName,
asyncTriggerParameters.getBuildType().toString(),
asyncTriggerParameters.getCustomDataStorage(),
asyncTriggerParameters.getTriggerDescriptor().getProperties());
}

@NotNull
public CheckResult<MachineAddedSpec> createCrashOnSubmitResult(@NotNull Throwable throwable) {
return MachineAddedSpecCheckResult.createThrowableResult(throwable);
}

public String describeTrigger(BuildTriggerDescriptor buildTriggerDescriptor) {
Map<String, String> properties = buildTriggerDescriptor.getProperties();
return String.format("Wait for a new machine to be added to server %s.",
properties.get(OCTOPUS_URL));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.mjrichardson.teamCity.buildTriggers.MachineAdded;

import com.intellij.openapi.diagnostic.Logger;
import com.mjrichardson.teamCity.buildTriggers.OctopusBuildTriggerUtil;
import jetbrains.buildServer.buildTriggers.BuildTriggerDescriptor;
import jetbrains.buildServer.buildTriggers.BuildTriggerService;
import jetbrains.buildServer.buildTriggers.BuildTriggeringPolicy;
import jetbrains.buildServer.buildTriggers.async.AsyncBuildTrigger;
import jetbrains.buildServer.buildTriggers.async.AsyncBuildTriggerFactory;
import jetbrains.buildServer.serverSide.PropertiesProcessor;
import jetbrains.buildServer.web.openapi.PluginDescriptor;
import org.jetbrains.annotations.NotNull;

public final class MachineAddedBuildTriggerService extends BuildTriggerService {
@NotNull
private static final Logger LOG = Logger.getInstance(MachineAddedBuildTriggerService.class.getName());
@NotNull
private final PluginDescriptor myPluginDescriptor;
@NotNull
private final BuildTriggeringPolicy myPolicy;

public MachineAddedBuildTriggerService(@NotNull final PluginDescriptor pluginDescriptor,
@NotNull final AsyncBuildTriggerFactory triggerFactory) {
myPluginDescriptor = pluginDescriptor;
myPolicy = triggerFactory.createBuildTrigger(MachineAddedSpec.class, getAsyncBuildTrigger(), LOG, getPollInterval());
}

@NotNull
@Override
public String getName() {
return "octopusMachineAddedTrigger";
}

@NotNull
@Override
public String getDisplayName() {
return "Octopus Machine Added Trigger";
}

@NotNull
@Override
public String describeTrigger(@NotNull BuildTriggerDescriptor buildTriggerDescriptor) {
return getBuildTrigger().describeTrigger(buildTriggerDescriptor);
}

@NotNull
@Override
public BuildTriggeringPolicy getBuildTriggeringPolicy() {
return myPolicy;
}

@Override
public PropertiesProcessor getTriggerPropertiesProcessor() {
return new MachineAddedTriggerPropertiesProcessor();
}

@Override
public String getEditParametersUrl() {
return myPluginDescriptor.getPluginResourcesPath("editOctopusMachineAddedTrigger.jsp");
}

@Override
public boolean isMultipleTriggersPerBuildTypeAllowed() {
return true;
}

@NotNull
private AsyncBuildTrigger<MachineAddedSpec> getAsyncBuildTrigger() {
return getBuildTrigger();
}

@NotNull
private int getPollInterval() {
return OctopusBuildTriggerUtil.getPollInterval();
}

@NotNull
private MachineAddedAsyncBuildTrigger getBuildTrigger() {
return new MachineAddedAsyncBuildTrigger(getDisplayName(), getPollInterval());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.mjrichardson.teamCity.buildTriggers.MachineAdded;

import com.intellij.openapi.diagnostic.Logger;
import com.mjrichardson.teamCity.buildTriggers.OctopusBuildTriggerUtil;
import jetbrains.buildServer.buildTriggers.BuildTriggerDescriptor;
import jetbrains.buildServer.buildTriggers.async.CheckJob;
import jetbrains.buildServer.buildTriggers.async.CheckResult;
import jetbrains.buildServer.serverSide.CustomDataStorage;
import jetbrains.buildServer.util.StringUtil;
import org.jetbrains.annotations.NotNull;

import java.util.Map;

import static com.mjrichardson.teamCity.buildTriggers.OctopusBuildTriggerUtil.*;

class MachineAddedCheckJob implements CheckJob<MachineAddedSpec> {
@NotNull
private static final Logger LOG = Logger.getInstance(MachineAddedCheckJob.class.getName());

private final MachinesProviderFactory MachinesProviderFactory;
private final String displayName;
private final String buildType;
private final CustomDataStorage dataStorage;
private final Map<String, String> props;

public MachineAddedCheckJob(String displayName, String buildType, CustomDataStorage dataStorage, Map<String, String> properties) {
this(new MachinesProviderFactory(), displayName, buildType, dataStorage, properties);
}

public MachineAddedCheckJob(MachinesProviderFactory MachinesProviderFactory, String displayName, String buildType, CustomDataStorage dataStorage, Map<String, String> properties) {
this.MachinesProviderFactory = MachinesProviderFactory;
this.displayName = displayName;
this.buildType = buildType;
this.dataStorage = dataStorage;
this.props = properties;
}

@NotNull
CheckResult<MachineAddedSpec> getCheckResult(String octopusUrl, String octopusApiKey, CustomDataStorage dataStorage) {
LOG.debug("Checking for new machines for on server " + octopusUrl);
final String dataStorageKey = (displayName + "|" + octopusUrl).toLowerCase();

try {
String oldStoredData = dataStorage.getValue(dataStorageKey);
final Machines oldMachines = new Machines(oldStoredData);
final Integer connectionTimeoutInMilliseconds = OctopusBuildTriggerUtil.getConnectionTimeoutInMilliseconds();

MachinesProvider provider = MachinesProviderFactory.getProvider(octopusUrl, octopusApiKey, connectionTimeoutInMilliseconds);
final Machines newMachines = provider.getMachines();

//only store that one machine was added here, not multiple.
//otherwise, we could inadvertently miss new machines
//todo: move inside Machines class
final Machine newMachine = newMachines.getNextMachine(oldMachines);
final Machines trimmedMachines = new Machines(oldStoredData);
trimmedMachines.add(newMachine);
final String newStoredData = trimmedMachines.toString();

if (!newStoredData.equals(oldStoredData)) {
dataStorage.putValue(dataStorageKey, newStoredData);

//todo: see if its possible to to check the property on the context that says whether its new?
//http://javadoc.jetbrains.net/teamcity/openapi/current/jetbrains/buildServer/buildTriggers/PolledTriggerContext.html#getPreviousCallTime()
//do not trigger build after first adding trigger (oldMachines == null)
if (oldStoredData == null) {
LOG.debug("No previously known machines known for server " + octopusUrl + ": null" + " -> " + newStoredData);
return MachineAddedSpecCheckResult.createEmptyResult();
}

LOG.info("New Machine " + newMachine.name + " created on " + octopusUrl + ": " + oldStoredData + " -> " + newStoredData);
final MachineAddedSpec MachineAddedSpec = new MachineAddedSpec(octopusUrl, newMachine.name);
//todo: investigate passing multiple bits to createUpdatedResult()
return MachineAddedSpecCheckResult.createUpdatedResult(MachineAddedSpec);
}

LOG.info("No new machines on " + octopusUrl + ": " + oldStoredData + " -> " + newStoredData);
return MachineAddedSpecCheckResult.createEmptyResult();

} catch (Exception e) {
return MachineAddedSpecCheckResult.createThrowableResult(e);
}
}

@NotNull
public CheckResult<MachineAddedSpec> perform() {

final String octopusUrl = props.get(OCTOPUS_URL);
if (StringUtil.isEmptyOrSpaces(octopusUrl)) {
return MachineAddedSpecCheckResult.createErrorResult(String.format("%s settings are invalid (empty url) in build configuration %s",
displayName, buildType));
}

final String octopusApiKey = props.get(OCTOPUS_APIKEY);
if (StringUtil.isEmptyOrSpaces(octopusApiKey)) {
return MachineAddedSpecCheckResult.createErrorResult(String.format("%s settings are invalid (empty api key) in build configuration %s",
displayName, buildType));
}

return getCheckResult(octopusUrl, octopusApiKey, dataStorage);
}

public boolean allowSchedule(@NotNull BuildTriggerDescriptor buildTriggerDescriptor) {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.mjrichardson.teamCity.buildTriggers.MachineAdded;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

class MachineAddedSpec {
@NotNull
private final String url;
@Nullable
private final String name;

// MachineAddedSpec(@NotNull String url) {
// this(url, null);
// }

MachineAddedSpec(@NotNull String url, @Nullable String name) {
this.url = url;
this.name = name;
}

public String getRequestorString() {
// if (name == null)
// return String.format("Machine %s added to %s", name, url);
return String.format("Machine %s added to %s", name, url);
}
}
Loading

0 comments on commit 4376c5e

Please sign in to comment.