├── container-cloud ├── .idea │ ├── copyright │ │ └── profiles_settings.xml │ ├── vcs.xml │ ├── modules.xml │ ├── misc.xml │ └── compiler.xml ├── container-cloud-server │ ├── src │ │ └── main │ │ │ ├── java │ │ │ └── se │ │ │ │ └── capeit │ │ │ │ └── dev │ │ │ │ └── containercloud │ │ │ │ ├── cloud │ │ │ │ ├── providers │ │ │ │ │ ├── ContainerProviderSettingsValidator.java │ │ │ │ │ ├── ContainerProviderPropertiesProcessor.java │ │ │ │ │ ├── ContainerInstanceInfoProvider.java │ │ │ │ │ ├── ContainerProvider.java │ │ │ │ │ ├── TestConnectionResult.java │ │ │ │ │ ├── ContainerProviderFactory.java │ │ │ │ │ ├── DockerSocketContainerProvider.java │ │ │ │ │ └── HeliosContainerProvider.java │ │ │ │ ├── ContainerCloudImage.java │ │ │ │ ├── ContainerCloudClientFactory.java │ │ │ │ ├── ContainerCloudInstance.java │ │ │ │ ├── ContainerCloudConstants.java │ │ │ │ ├── ContainerCloudProfileController.java │ │ │ │ └── ContainerCloudClient.java │ │ │ │ └── feature │ │ │ │ ├── RunInContainerCloudConstants.java │ │ │ │ ├── RunInContainerCloudController.java │ │ │ │ ├── RunInContainerCloudBuildQueuedListener.java │ │ │ │ └── RunInContainerCloudBuildFeature.java │ │ │ └── resources │ │ │ ├── META-INF │ │ │ └── build-server-plugin-container-cloud.xml │ │ │ └── buildServerResources │ │ │ ├── feature-settings.jsp │ │ │ └── profile-settings.jsp │ ├── pom.xml │ └── container-cloud-server.iml ├── container-cloud-agent │ ├── teamcity-plugin.xml │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── build-agent-plugin-container-cloud.xml │ │ │ └── java │ │ │ └── se │ │ │ └── capeit │ │ │ └── dev │ │ │ └── containercloud │ │ │ └── agent │ │ │ └── ContainerCloudAgentPropertiesSetter.java │ ├── pom.xml │ └── container-cloud-agent.iml ├── container-cloud.iml ├── .gitignore ├── teamcity-plugin.xml ├── build │ ├── plugin-agent-assembly.xml │ ├── plugin-assembly.xml │ ├── pom.xml │ └── build.iml └── pom.xml ├── README.md └── LICENSE /container-cloud/.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /container-cloud/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/providers/ContainerProviderSettingsValidator.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud.providers; 2 | 3 | import java.util.Map; 4 | 5 | public interface ContainerProviderSettingsValidator { 6 | Map validateSettings(); 7 | } 8 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-agent/teamcity-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/providers/ContainerProviderPropertiesProcessor.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud.providers; 2 | 3 | import jetbrains.buildServer.serverSide.PropertiesProcessor; 4 | 5 | public interface ContainerProviderPropertiesProcessor { 6 | PropertiesProcessor getPropertiesProcessor(); 7 | } 8 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-agent/src/main/resources/META-INF/build-agent-plugin-container-cloud.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/providers/ContainerInstanceInfoProvider.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud.providers; 2 | 3 | import jetbrains.buildServer.clouds.InstanceStatus; 4 | 5 | import java.util.Date; 6 | 7 | public interface ContainerInstanceInfoProvider { 8 | String getError(String instanceId); 9 | 10 | String getNetworkIdentity(String instanceId); 11 | 12 | Date getStartedTime(String instanceId); 13 | 14 | InstanceStatus getStatus(String instanceId); 15 | } 16 | -------------------------------------------------------------------------------- /container-cloud/container-cloud.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /container-cloud/.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | target/ 4 | 5 | # Package Files # 6 | *.jar 7 | *.war 8 | *.ear 9 | 10 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 11 | hs_err_pid* 12 | 13 | .idea/workspace.xml 14 | .idea/tasks.xml 15 | 16 | # Sensitive or high-churn files: 17 | .idea/dataSources.ids 18 | .idea/dataSources.xml 19 | .idea/dataSources.local.xml 20 | .idea/sqlDataSources.xml 21 | .idea/dynamic.xml 22 | .idea/uiDesigner.xml 23 | 24 | # Gradle: 25 | .idea/gradle.xml 26 | .idea/libraries 27 | .idea/inspectionProfiles 28 | # Mongo Explorer plugin: 29 | .idea/mongoSettings.xml 30 | 31 | ## File-based project format: 32 | *.iws 33 | -------------------------------------------------------------------------------- /container-cloud/teamcity-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | container-cloud 5 | container-cloud 6 | @Version@ 7 | Run agents in containers 8 | https://github.com/carlpett/teamcity-container-cloud 9 | carlpett@users.noreply.github.com 10 | 11 | Capeit 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/providers/ContainerProvider.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud.providers; 2 | 3 | import jetbrains.buildServer.clouds.CloudInstanceUserData; 4 | import org.jetbrains.annotations.NotNull; 5 | import se.capeit.dev.containercloud.cloud.ContainerCloudImage; 6 | import se.capeit.dev.containercloud.cloud.ContainerCloudInstance; 7 | 8 | public interface ContainerProvider { 9 | ContainerCloudInstance startInstance(@NotNull String instanceId, @NotNull ContainerCloudImage image, @NotNull CloudInstanceUserData tag); 10 | 11 | void stopInstance(@NotNull ContainerCloudInstance instance); 12 | 13 | TestConnectionResult testConnection(); 14 | } 15 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/resources/META-INF/build-server-plugin-container-cloud.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /container-cloud/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/feature/RunInContainerCloudConstants.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.feature; 2 | 3 | public final class RunInContainerCloudConstants { 4 | public static final String TYPE = "RunInContainerCloud"; 5 | public static final String FeatureSettingsJspFile = "feature-settings.jsp"; 6 | public static final String FeatureSettingsHtmlFile = "feature-settings.html"; 7 | 8 | public static final String ParameterName_CloudProfile = "CloudProfile"; 9 | public static final String ParameterName_Image = "Image"; 10 | 11 | // JSP getters 12 | public String getParameterName_CloudProfile() { 13 | return ParameterName_CloudProfile; 14 | } 15 | 16 | public String getParameterName_Image() { 17 | return ParameterName_Image; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /container-cloud/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /container-cloud/build/plugin-agent-assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | plugin-agent-assembly 4 | false 5 | 6 | zip 7 | 8 | 9 | 10 | ../container-cloud-agent/teamcity-plugin.xml 11 | / 12 | 13 | 14 | 15 | 16 | true 17 | 18 | se.capeit.dev:container-cloud-agent 19 | 20 | 21 | lib 22 | false 23 | 24 | 25 | 26 | * 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /container-cloud/build/plugin-assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | plugin-assembly 4 | false 5 | 6 | zip 7 | 8 | 9 | 10 | target/teamcity-plugin.xml 11 | / 12 | 13 | 14 | target/container-cloud-agent.zip 15 | agent 16 | 17 | 18 | 19 | 20 | true 21 | 22 | se.capeit.dev:container-cloud-server 23 | 24 | 25 | server 26 | false 27 | 28 | 29 | 30 | * 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/providers/TestConnectionResult.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud.providers; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class TestConnectionResult { 7 | private boolean ok; 8 | private final List messages; 9 | 10 | public TestConnectionResult() { 11 | this.ok = false; 12 | this.messages = new ArrayList<>(); 13 | } 14 | 15 | public void setOk(boolean isOk) { 16 | ok = isOk; 17 | } 18 | 19 | public boolean isOk() { 20 | return ok; 21 | } 22 | 23 | public void addMessage(String msg, Message.MessageLevel level) { 24 | messages.add(new Message(msg, level)); 25 | } 26 | 27 | public List getMessages() { 28 | return messages; 29 | } 30 | 31 | public static class Message { 32 | private final String message; 33 | private final MessageLevel level; 34 | 35 | Message(String message, MessageLevel level) { 36 | this.message = message; 37 | this.level = level; 38 | } 39 | 40 | public String getMessage() { 41 | return message; 42 | } 43 | 44 | public MessageLevel getLevel() { 45 | return level; 46 | } 47 | 48 | public enum MessageLevel { INFO, WARNING, ERROR } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/resources/buildServerResources/feature-settings.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="props" tagdir="/WEB-INF/tags/props" %> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | ${profile.value} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /container-cloud/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-agent/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | container-cloud 7 | se.capeit.dev 8 | 1.0-SNAPSHOT 9 | 10 | 11 | container-cloud-agent 12 | 13 | jar 14 | 15 | 16 | org.jetbrains.teamcity 17 | agent-api 18 | ${teamcity-version} 19 | provided 20 | 21 | 22 | 23 | org.jetbrains.teamcity 24 | agent 25 | ${teamcity-version} 26 | provided 27 | 28 | 29 | org.jetbrains.teamcity 30 | cloud-shared 31 | ${teamcity-version} 32 | provided 33 | 34 | 35 | org.jetbrains.teamcity 36 | common-api 37 | ${teamcity-version} 38 | provided 39 | 40 | 41 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-agent/src/main/java/se/capeit/dev/containercloud/agent/ContainerCloudAgentPropertiesSetter.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.agent; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import jetbrains.buildServer.agent.AgentLifeCycleAdapter; 5 | import jetbrains.buildServer.agent.AgentLifeCycleListener; 6 | import jetbrains.buildServer.agent.BuildAgent; 7 | import jetbrains.buildServer.agent.BuildAgentConfigurationEx; 8 | import jetbrains.buildServer.clouds.CloudConstants; 9 | import jetbrains.buildServer.log.Loggers; 10 | import jetbrains.buildServer.util.EventDispatcher; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | public class ContainerCloudAgentPropertiesSetter { 14 | private static final Logger LOG = Loggers.AGENT; // Logger.getInstance(ContainerCloudInstance.class.getName()); 15 | 16 | private final BuildAgentConfigurationEx agentConfiguration; 17 | @NotNull 18 | private final EventDispatcher events; 19 | 20 | public ContainerCloudAgentPropertiesSetter(final BuildAgentConfigurationEx agentConfiguration, 21 | @NotNull EventDispatcher events) { 22 | LOG.info("Created ContainerCloudAgentPropertiesSetter"); 23 | this.agentConfiguration = agentConfiguration; 24 | this.events = events; 25 | 26 | events.addListener(new AgentLifeCycleAdapter() { 27 | @Override 28 | public void afterAgentConfigurationLoaded(@NotNull BuildAgent buildAgent) { 29 | LOG.info("ContainerCloudAgentPropertiesSetter: Setting terminate after build flag"); 30 | agentConfiguration.addConfigurationParameter(CloudConstants.AGENT_TERMINATE_AFTER_BUILD, "true"); 31 | } 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /container-cloud/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | se.capeit.dev 6 | container-cloud 7 | 1.0-SNAPSHOT 8 | pom 9 | 10 | 10.0 11 | 12 | 13 | 14 | JetBrains 15 | http://repository.jetbrains.com/all 16 | 17 | 18 | 19 | 20 | JetBrains 21 | http://repository.jetbrains.com/all 22 | 23 | 24 | 25 | 26 | 27 | 28 | org.apache.maven.plugins 29 | maven-compiler-plugin 30 | 31 | 1.8 32 | 1.8 33 | 34 | 35 | 36 | org.jetbrains.teamcity 37 | teamcity-sdk-maven-plugin 38 | 0.2 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | container-cloud-server 48 | container-cloud-agent 49 | build 50 | 51 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/ContainerCloudImage.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import jetbrains.buildServer.clouds.CloudErrorInfo; 5 | import jetbrains.buildServer.clouds.CloudImage; 6 | import jetbrains.buildServer.clouds.CloudInstance; 7 | import jetbrains.buildServer.log.Loggers; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.util.Collection; 11 | import java.util.Collections; 12 | import java.util.Map; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | 15 | public class ContainerCloudImage implements CloudImage { 16 | private static final Logger LOG = Loggers.SERVER; // Logger.getInstance(ContainerCloudImage.class.getName()); 17 | private final String imageId; 18 | private final Map instances; 19 | // TODO: Consider overriding hashCode? 20 | 21 | public ContainerCloudImage(String imageId) { 22 | this.imageId = imageId; 23 | this.instances = new ConcurrentHashMap<>(); 24 | } 25 | 26 | // Finds instance by instanceId 27 | public CloudInstance findInstanceById(@NotNull String id) { 28 | return instances.get(id); 29 | } 30 | 31 | // Returns the identifier of this image, in the vendor-specific form. 32 | @NotNull 33 | public String getId() { 34 | return imageId; 35 | } 36 | 37 | // Returns all instances of running image 38 | @NotNull 39 | public Collection getInstances() { 40 | return Collections.unmodifiableCollection(instances.values()); 41 | } 42 | 43 | public void registerInstance(ContainerCloudInstance instance) { 44 | instances.put(instance.getInstanceId(), instance); 45 | } 46 | 47 | public void unregisterInstance(@NotNull String instanceId) { 48 | instances.remove(instanceId); 49 | } 50 | 51 | @NotNull 52 | public String getName() { 53 | return imageId; 54 | } 55 | 56 | // Returns error information of there was an error. 57 | public CloudErrorInfo getErrorInfo() { 58 | return null; 59 | } 60 | 61 | public Integer getAgentPoolId() { 62 | LOG.info("getAgentPoolId"); 63 | return 0; 64 | } 65 | } -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/feature/RunInContainerCloudController.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.feature; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import jetbrains.buildServer.clouds.CloudProfile; 5 | import jetbrains.buildServer.clouds.CloudProfileData; 6 | import jetbrains.buildServer.clouds.server.CloudManager; 7 | import jetbrains.buildServer.controllers.BaseController; 8 | import jetbrains.buildServer.log.Loggers; 9 | import jetbrains.buildServer.serverSide.SBuildServer; 10 | import jetbrains.buildServer.web.openapi.PluginDescriptor; 11 | import jetbrains.buildServer.web.openapi.WebControllerManager; 12 | import org.jetbrains.annotations.NotNull; 13 | import org.jetbrains.annotations.Nullable; 14 | import org.springframework.web.servlet.ModelAndView; 15 | import se.capeit.dev.containercloud.cloud.ContainerCloudConstants; 16 | 17 | import javax.servlet.http.HttpServletRequest; 18 | import javax.servlet.http.HttpServletResponse; 19 | import java.util.Map; 20 | import java.util.stream.Collectors; 21 | 22 | public class RunInContainerCloudController extends BaseController { 23 | private static final Logger LOG = Loggers.SERVER; // Logger.getInstance(ContainerCloudClient.class.getName()); 24 | 25 | @NotNull 26 | private final CloudManager cloudManager; 27 | private final PluginDescriptor pluginDescriptor; 28 | 29 | public RunInContainerCloudController(@NotNull SBuildServer server, 30 | @NotNull PluginDescriptor pluginDescriptor, 31 | @NotNull WebControllerManager webControllerManager, 32 | @NotNull CloudManager cloudManager) { 33 | super(server); 34 | 35 | this.cloudManager = cloudManager; 36 | this.pluginDescriptor = pluginDescriptor; 37 | 38 | webControllerManager.registerController(pluginDescriptor.getPluginResourcesPath(RunInContainerCloudConstants.FeatureSettingsHtmlFile), this); 39 | } 40 | 41 | @Nullable 42 | @Override 43 | protected ModelAndView doHandle(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse) throws Exception { 44 | ModelAndView mv = new ModelAndView(pluginDescriptor.getPluginResourcesPath(RunInContainerCloudConstants.FeatureSettingsJspFile)); 45 | 46 | mv.getModel().put("cloudProfiles", getProfiles()); 47 | 48 | return mv; 49 | } 50 | 51 | private Map getProfiles() { 52 | return cloudManager.listProfiles().stream() 53 | .filter(cloudProfile -> cloudProfile.getCloudCode().equals(ContainerCloudConstants.CloudCode)) 54 | .collect(Collectors.toMap(CloudProfile::getProfileId, CloudProfileData::getProfileName)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/providers/ContainerProviderFactory.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud.providers; 2 | 3 | import jetbrains.buildServer.clouds.CloudClientParameters; 4 | import jetbrains.buildServer.clouds.CloudException; 5 | import jetbrains.buildServer.serverSide.InvalidProperty; 6 | import jetbrains.buildServer.serverSide.PropertiesProcessor; 7 | import se.capeit.dev.containercloud.cloud.ContainerCloudConstants; 8 | 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | 12 | public class ContainerProviderFactory { 13 | public static ContainerProvider getProvider(CloudClientParameters parameters) { 14 | String provider = parameters.getParameter(ContainerCloudConstants.ProfileParameterName_ContainerProvider); 15 | switch (provider) { 16 | case ContainerCloudConstants.ProfileParameterValue_ContainerProvider_DockerSocket: 17 | return new DockerSocketContainerProvider(parameters); 18 | case ContainerCloudConstants.ProfileParameterValue_ContainerProvider_Helios: 19 | return new HeliosContainerProvider(parameters); 20 | default: 21 | throw new CloudException("Unknown container provider '" + provider + "'"); 22 | } 23 | } 24 | 25 | public static PropertiesProcessor getPropertiesProcessor() { 26 | return properties -> { 27 | if (!properties.containsKey(ContainerCloudConstants.ProfileParameterName_ContainerProvider)) { 28 | // There's no use trying to validate anything more at this point (shouldn't ever happen anayway...) 29 | return Stream.of( 30 | new InvalidProperty(ContainerCloudConstants.ProfileParameterName_ContainerProvider, 31 | "Container provider not selected")) 32 | .collect(Collectors.toList()); 33 | } 34 | 35 | PropertiesProcessor specificProcessor; 36 | String provider = properties.get(ContainerCloudConstants.ProfileParameterName_ContainerProvider); 37 | switch (provider) { 38 | case ContainerCloudConstants.ProfileParameterValue_ContainerProvider_DockerSocket: 39 | specificProcessor = DockerSocketContainerProvider.getPropertiesProcessor(); 40 | break; 41 | case ContainerCloudConstants.ProfileParameterValue_ContainerProvider_Helios: 42 | specificProcessor = HeliosContainerProvider.getPropertiesProcessor(); 43 | break; 44 | default: 45 | throw new CloudException("Unknown container provider '" + provider + "'"); 46 | } 47 | 48 | return specificProcessor.process(properties); 49 | }; 50 | } 51 | } -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/feature/RunInContainerCloudBuildQueuedListener.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.feature; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import jetbrains.buildServer.clouds.CloudClientEx; 5 | import jetbrains.buildServer.clouds.server.CloudManager; 6 | import jetbrains.buildServer.log.Loggers; 7 | import jetbrains.buildServer.serverSide.*; 8 | import org.jetbrains.annotations.NotNull; 9 | import se.capeit.dev.containercloud.cloud.ContainerCloudClient; 10 | 11 | import java.util.Collection; 12 | import java.util.Map; 13 | import java.util.Optional; 14 | 15 | public class RunInContainerCloudBuildQueuedListener extends BuildServerAdapter { 16 | private static final Logger LOG = Loggers.SERVER; // Logger.getInstance(ContainerCloudClient.class.getName()); 17 | 18 | private final CloudManager cloudManager; 19 | 20 | public RunInContainerCloudBuildQueuedListener(@NotNull SBuildServer server, @NotNull CloudManager cloudManager) { 21 | this.cloudManager = cloudManager; 22 | 23 | server.addListener(this); 24 | } 25 | 26 | @Override 27 | public void buildTypeAddedToQueue(@NotNull SQueuedBuild queuedBuild) { 28 | // This is added as a sort of "last resort" to make sure versioned settings etc don't bypass the 29 | // buildTypePersisted event and leave builds hanging in the queue. 30 | addImageForBuildType(queuedBuild.getBuildType()); 31 | } 32 | 33 | @Override 34 | public void buildTypePersisted(@NotNull SBuildType buildType) { 35 | addImageForBuildType(buildType); 36 | } 37 | 38 | private void addImageForBuildType(@NotNull SBuildType buildType) { 39 | Collection featureDescriptors = buildType.getBuildFeaturesOfType(RunInContainerCloudConstants.TYPE); 40 | // There is either zero or one feature 41 | Optional feature = featureDescriptors.stream().findAny(); 42 | 43 | // If there is no RunInContainerCloudFeature, do nothing 44 | if (!feature.isPresent()) { 45 | return; 46 | } 47 | 48 | // There is a feature, so get the parameters 49 | Map parameters = feature.get().getParameters(); 50 | String profileId = parameters.get(RunInContainerCloudConstants.ParameterName_CloudProfile); 51 | CloudClientEx clientEx = cloudManager.getClientIfExists(profileId); 52 | if (clientEx == null || !(clientEx instanceof ContainerCloudClient)) { 53 | LOG.warn("BuildType " + buildType.getConfigId() + " has a RunInContainerCloud feature indicating profile " + profileId + ", but there is no such cloud client registered"); 54 | return; 55 | } 56 | 57 | LOG.info("Adding image " + parameters.get(RunInContainerCloudConstants.ParameterName_Image) + " to profile " + profileId); 58 | ContainerCloudClient containerClient = (ContainerCloudClient) clientEx; 59 | containerClient.addImage(parameters.get(RunInContainerCloudConstants.ParameterName_Image)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/ContainerCloudClientFactory.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import jetbrains.buildServer.clouds.*; 5 | import jetbrains.buildServer.log.Loggers; 6 | import jetbrains.buildServer.serverSide.PropertiesProcessor; 7 | import jetbrains.buildServer.web.openapi.PluginDescriptor; 8 | import org.jetbrains.annotations.NotNull; 9 | import se.capeit.dev.containercloud.cloud.providers.ContainerProviderFactory; 10 | 11 | import java.util.Collections; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | 16 | public class ContainerCloudClientFactory implements CloudClientFactory { 17 | private static final Logger LOG = Loggers.SERVER; // Logger.getInstance(ContainerCloudClientFactory.class.getName()); 18 | private final String editProfilePath; 19 | 20 | public ContainerCloudClientFactory(final CloudRegistrar cloudRegistrar, 21 | final PluginDescriptor pluginDescriptor) { 22 | editProfilePath = pluginDescriptor.getPluginResourcesPath(ContainerCloudConstants.ProfileSettingsJspFile); 23 | cloudRegistrar.registerCloudFactory(this); 24 | } 25 | 26 | @NotNull 27 | public ContainerCloudClient createNewClient(@NotNull final CloudState state, @NotNull final CloudClientParameters params) { 28 | try { 29 | return new ContainerCloudClient(state, params); 30 | } catch (Exception e) { 31 | throw new CloudException("Failed to create new Container Cloud client", e); 32 | } 33 | } 34 | 35 | // Checks if the agent could be an instance of one of the running profiles. 36 | public boolean canBeAgentOfType(@NotNull jetbrains.buildServer.serverSide.AgentDescription description) { 37 | // TODO: Shouldn't this actually be false? 38 | LOG.info("ClientCloudClientFactory.canBeAgentOfType " + description.toString() + ", hardcoded true!"); 39 | return true; 40 | } 41 | 42 | // The formal name of the cloud type. 43 | @NotNull 44 | public String getCloudCode() { 45 | return ContainerCloudConstants.CloudCode; 46 | 47 | } 48 | 49 | // Description to be shown on the web pages 50 | @NotNull 51 | public String getDisplayName() { 52 | return "Container Cloud"; 53 | } 54 | 55 | // Properties editor jsp 56 | public String getEditProfileUrl() { 57 | return editProfilePath; 58 | } 59 | 60 | // Return initial values for form parameters. 61 | @NotNull 62 | public Map getInitialParameterValues() { 63 | Map initialValues = new HashMap<>(); 64 | initialValues.put(ContainerCloudConstants.ProfileParameterName_Images, "[]"); // Default to empty JSON array 65 | return initialValues; 66 | } 67 | 68 | // Returns the properties processor instance (validator). 69 | @NotNull 70 | public PropertiesProcessor getPropertiesProcessor() { 71 | return ContainerProviderFactory.getPropertiesProcessor(); 72 | } 73 | } -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/ContainerCloudInstance.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud; 2 | 3 | import com.google.common.base.Strings; 4 | import com.intellij.openapi.diagnostic.Logger; 5 | import jetbrains.buildServer.clouds.CloudErrorInfo; 6 | import jetbrains.buildServer.clouds.CloudImage; 7 | import jetbrains.buildServer.clouds.CloudInstance; 8 | import jetbrains.buildServer.clouds.InstanceStatus; 9 | import jetbrains.buildServer.log.Loggers; 10 | import org.jetbrains.annotations.NotNull; 11 | import se.capeit.dev.containercloud.cloud.providers.ContainerInstanceInfoProvider; 12 | 13 | import java.util.Date; 14 | import java.util.Map; 15 | 16 | public class ContainerCloudInstance implements CloudInstance { 17 | private static final Logger LOG = Loggers.SERVER; // Logger.getInstance(ContainerCloudInstance.class.getName()); 18 | private final String id; 19 | private final ContainerCloudImage image; 20 | private final ContainerInstanceInfoProvider infoProvider; 21 | 22 | public ContainerCloudInstance(String id, ContainerCloudImage image, ContainerInstanceInfoProvider infoProvider) { 23 | this.id = id; 24 | this.image = image; 25 | this.infoProvider = infoProvider; 26 | } 27 | 28 | // Checks is the agent is running under this instance 29 | public boolean containsAgent(@NotNull jetbrains.buildServer.serverSide.AgentDescription agent) { 30 | Map configParams = agent.getConfigurationParameters(); 31 | return getInstanceId().equals(configParams.get(ContainerCloudConstants.AgentEnvParameterName_InstanceId)) && 32 | getImageId().equals(configParams.get(ContainerCloudConstants.AgentEnvParameterName_ImageId)); 33 | } 34 | 35 | // Returns correct error info if getStatus() returns InstanceStatus.ERROR value. 36 | public CloudErrorInfo getErrorInfo() { 37 | try { 38 | String error = infoProvider.getError(id); 39 | if (Strings.isNullOrEmpty(error)) { 40 | return null; 41 | } 42 | return new CloudErrorInfo(error); 43 | } catch (Exception e) { 44 | LOG.error("Could not fetch error info", e); 45 | return new CloudErrorInfo("Failed to get error info", e.getMessage(), e); 46 | } 47 | } 48 | 49 | // Returns the reference to the handle of the image this instance started from. 50 | @NotNull 51 | public CloudImage getImage() { 52 | return image; 53 | } 54 | 55 | // Returns the image identifier 56 | @NotNull 57 | public String getImageId() { 58 | return image.getId(); 59 | } 60 | 61 | // Returns the instance identifier 62 | @NotNull 63 | public String getInstanceId() { 64 | return id; 65 | } 66 | 67 | // Name of the instance. 68 | @NotNull 69 | public String getName() { 70 | return id; 71 | } 72 | 73 | // Returns the instance's DNS name (if one exists) or IPv4 address (if no DNS names). 74 | public String getNetworkIdentity() { 75 | return infoProvider.getNetworkIdentity(id); 76 | } 77 | 78 | // Returns the instance started time. 79 | @NotNull 80 | public Date getStartedTime() { 81 | return infoProvider.getStartedTime(id); 82 | } 83 | 84 | // current status of the instance 85 | @NotNull 86 | public InstanceStatus getStatus() { 87 | return infoProvider.getStatus(id); 88 | } 89 | } -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | container-cloud 7 | se.capeit.dev 8 | 1.0-SNAPSHOT 9 | 10 | 11 | container-cloud-server 12 | 13 | jar 14 | 15 | 16 | org.jetbrains.teamcity 17 | server-api 18 | ${teamcity-version} 19 | provided 20 | 21 | 22 | 23 | org.jetbrains.teamcity 24 | server-web-api 25 | ${teamcity-version} 26 | war 27 | provided 28 | 29 | 30 | 31 | org.jetbrains.teamcity 32 | cloud-interface 33 | ${teamcity-version} 34 | provided 35 | 36 | 37 | 38 | org.jetbrains.teamcity 39 | cloud-shared 40 | ${teamcity-version} 41 | provided 42 | 43 | 44 | 45 | 46 | org.jetbrains.teamcity 47 | cloud-server 48 | ${teamcity-version} 49 | provided 50 | 51 | 52 | 53 | com.spotify 54 | docker-client 55 | 6.0.0 56 | compile 57 | 58 | 59 | 60 | com.spotify 61 | helios-client 62 | 0.9.53 63 | compile 64 | 65 | 66 | 68 | 69 | javax.ws.rs 70 | jsr311-api 71 | 1.1.1 72 | provided 73 | 74 | 75 | 76 | 77 | org.slf4j 78 | slf4j-api 79 | 1.7.5 80 | provided 81 | 82 | 83 | 84 | org.jetbrains.teamcity 85 | tests-support 86 | ${teamcity-version} 87 | test 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /container-cloud/build/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | container-cloud 7 | se.capeit.dev 8 | 1.0-SNAPSHOT 9 | 10 | build 11 | pom 12 | 13 | 14 | 15 | 16 | se.capeit.dev 17 | container-cloud-server 18 | 1.0-SNAPSHOT 19 | 20 | 21 | 22 | 23 | 24 | com.google.code.maven-replacer-plugin 25 | replacer 26 | 1.5.2 27 | 28 | 29 | process-sources 30 | 31 | replace 32 | 33 | 34 | 35 | 36 | ${basedir}/../teamcity-plugin.xml 37 | ${basedir}/target/teamcity-plugin.xml 38 | 39 | 40 | @Version@ 41 | ${project.parent.version} 42 | 43 | 44 | 45 | 46 | 47 | maven-assembly-plugin 48 | 2.4 49 | 50 | 51 | make-agent-assembly 52 | package 53 | 54 | single 55 | 56 | 57 | container-cloud-agent 58 | false 59 | 60 | plugin-agent-assembly.xml 61 | 62 | 63 | 64 | 65 | make-assembly 66 | package 67 | 68 | single 69 | 70 | 71 | container-cloud 72 | ${project.parent.build.directory} 73 | false 74 | 75 | plugin-assembly.xml 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/ContainerCloudConstants.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud; 2 | 3 | public final class ContainerCloudConstants { 4 | public static final String CloudCode = "Cntnr"; 5 | public static final String ProfileSettingsJspFile = "profile-settings.jsp"; 6 | public static final String ProfileSettingsTestConnectionPath = "profile-settings/test-connection.jsp"; 7 | 8 | public static final String ProfileParameterName_ContainerProvider = "ContainerProvider"; 9 | public static final String ProfileParameterName_Images = "Images"; 10 | 11 | public static final String ProfileParameterValue_ContainerProvider_DockerSocket = "docker-socket"; 12 | public static final String ProfileParameterValue_ContainerProvider_Helios = "helios"; 13 | 14 | public static final String ProfileParameterName_DockerSocket_Endpoint = "DockerSocket-Endpoint"; 15 | 16 | public static final String ProfileParameterName_Helios_MasterUrl = "Helios-MasterUrl"; 17 | public static final String ProfileParameterName_Helios_HostNamePattern = "Helios-HostNamePattern"; 18 | public static final String ProfileParameterName_Helios_HostSelectors = "Helios-HostSelectors"; 19 | 20 | public static final String AgentEnvParameterName_ImageId = "CONTAINER_CLOUD_AGENT_IMAGE_ID"; 21 | public static final String AgentEnvParameterName_InstanceId = "CONTAINER_CLOUD_INSTANCE_ID"; 22 | 23 | // This regular expression validates a container image, source: https://github.com/docker/distribution/blob/master/reference/regexp.go 24 | // The expression corresponds to what would be generated by the code anchored(NameRegexp, literal(":"), TagRegexp) 25 | public static final String ContainerImageRegex = "^(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?/)?[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?:[\\w][\\w.-]{0,127}$"; 26 | 27 | // JSP getters 28 | public String getCloudCode() { 29 | return CloudCode; 30 | } 31 | 32 | public String getProfileSettingsTestConnectionPath() { 33 | return ProfileSettingsTestConnectionPath; 34 | } 35 | 36 | public String getProfileParameterName_ContainerProvider() { 37 | return ProfileParameterName_ContainerProvider; 38 | } 39 | 40 | public String getProfileParameterName_Images() { 41 | return ProfileParameterName_Images; 42 | } 43 | 44 | public String getProfileParameterValue_ContainerProvider_DockerSocket() { 45 | return ProfileParameterValue_ContainerProvider_DockerSocket; 46 | } 47 | 48 | public String getProfileParameterValue_ContainerProvider_Helios() { 49 | return ProfileParameterValue_ContainerProvider_Helios; 50 | } 51 | 52 | public String getProfileParameterName_DockerSocket_Endpoint() { 53 | return ProfileParameterName_DockerSocket_Endpoint; 54 | } 55 | 56 | public String getProfileParameterName_Helios_MasterUrl() { 57 | return ProfileParameterName_Helios_MasterUrl; 58 | } 59 | 60 | public String getProfileParameterName_Helios_HostNamePattern() { 61 | return ProfileParameterName_Helios_HostNamePattern; 62 | } 63 | 64 | public String getProfileParameterName_Helios_HostSelectors() { 65 | return ProfileParameterName_Helios_HostSelectors; 66 | } 67 | 68 | public String getAgentEnvParameterName_ImageId() { 69 | return AgentEnvParameterName_ImageId; 70 | } 71 | 72 | public String getAgentEnvParameterName_InstanceId() { 73 | return AgentEnvParameterName_InstanceId; 74 | } 75 | 76 | public String getContainerImageRegex() { 77 | // Formatted for Javascript, so need to escape inner slashes 78 | return ContainerImageRegex.replace("/", "\\/"); 79 | } 80 | } -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/feature/RunInContainerCloudBuildFeature.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.feature; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import jetbrains.buildServer.clouds.server.CloudManager; 5 | import jetbrains.buildServer.log.Loggers; 6 | import jetbrains.buildServer.serverSide.BuildFeature; 7 | import jetbrains.buildServer.serverSide.InvalidProperty; 8 | import jetbrains.buildServer.serverSide.PropertiesProcessor; 9 | import jetbrains.buildServer.web.openapi.PluginDescriptor; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | import se.capeit.dev.containercloud.cloud.ContainerCloudConstants; 13 | 14 | import java.util.ArrayList; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | public class RunInContainerCloudBuildFeature extends BuildFeature { 19 | private static final Logger LOG = Loggers.SERVER; // Logger.getInstance(ContainerCloudClient.class.getName()); 20 | 21 | private final String editParametersPath; 22 | private final CloudManager cloudManager; 23 | 24 | public RunInContainerCloudBuildFeature(PluginDescriptor pluginDescriptor, CloudManager cloudManager) { 25 | this.editParametersPath = pluginDescriptor.getPluginResourcesPath(RunInContainerCloudConstants.FeatureSettingsHtmlFile); 26 | this.cloudManager = cloudManager; 27 | } 28 | 29 | @NotNull 30 | @Override 31 | public String getType() { 32 | return RunInContainerCloudConstants.TYPE; 33 | } 34 | 35 | @NotNull 36 | @Override 37 | public String getDisplayName() { 38 | return "Run in Container Cloud"; 39 | } 40 | 41 | @NotNull 42 | @Override 43 | public String describeParameters(@NotNull Map params) { 44 | StringBuilder sb = new StringBuilder(); 45 | 46 | String profileId = params.get(RunInContainerCloudConstants.ParameterName_CloudProfile); 47 | String profileName = cloudManager.findProfileById(profileId).getProfileName(); 48 | sb.append("Cloud profile: "); 49 | sb.append(profileName); 50 | 51 | sb.append("\nImage: "); 52 | sb.append(params.get(RunInContainerCloudConstants.ParameterName_Image)); 53 | 54 | return sb.toString(); 55 | } 56 | 57 | @Nullable 58 | @Override 59 | public PropertiesProcessor getParametersProcessor() { 60 | return properties -> { 61 | ArrayList toReturn = new ArrayList<>(); 62 | if (!properties.containsKey(RunInContainerCloudConstants.ParameterName_CloudProfile)) 63 | toReturn.add(new InvalidProperty(RunInContainerCloudConstants.ParameterName_CloudProfile, 64 | "Please choose a cloud profile")); 65 | 66 | if (!properties.containsKey(RunInContainerCloudConstants.ParameterName_Image) || 67 | properties.get(RunInContainerCloudConstants.ParameterName_Image).isEmpty()) 68 | toReturn.add(new InvalidProperty(RunInContainerCloudConstants.ParameterName_Image, 69 | "Please choose an image")); 70 | else if (!properties.get(RunInContainerCloudConstants.ParameterName_Image).matches(ContainerCloudConstants.ContainerImageRegex)) 71 | toReturn.add(new InvalidProperty(RunInContainerCloudConstants.ParameterName_Image, 72 | "Image must have format owner/image:version or repo-domain/owner/image:version (note that upper-case letters are not allowed)")); 73 | 74 | return toReturn; 75 | }; 76 | } 77 | 78 | @Nullable 79 | @Override 80 | public Map getDefaultParameters() { 81 | HashMap defaults = new HashMap<>(); 82 | defaults.put(RunInContainerCloudConstants.ParameterName_CloudProfile, ""); 83 | defaults.put(RunInContainerCloudConstants.ParameterName_Image, ""); 84 | 85 | return defaults; 86 | } 87 | 88 | @Nullable 89 | @Override 90 | public String getEditParametersUrl() { 91 | return editParametersPath; 92 | } 93 | 94 | @Override 95 | public boolean isMultipleFeaturesPerBuildTypeAllowed() { 96 | return false; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Teamcity Container Cloud plugin 2 | This plugin implements "Agent cloud" functionality for running agents within 3 | containers, such as Docker. 4 | 5 | This allows an easy way to customize the build environment for every build, 6 | instead of maintaining a set of generalized build agent that should build 7 | anything, or having dozens of mostly idle specialized agents. 8 | 9 | As a starting point to creating agent images, have a look at Jetbrain's 10 | [teamcity-agent](https://hub.docker.com/r/jetbrains/teamcity-agent/) and 11 | [teamcity-minimal-agent](https://hub.docker.com/r/jetbrains/teamcity-minimal-agent/) 12 | Docker containers. 13 | 14 | # Compatibility 15 | This plugin has been developed and tested against TeamCity 10 only, so far. 16 | 17 | # Installation 18 | The plugin is distributed as a single archive which TeamCity can load directly, 19 | `container-cloud.zip`. Either drop the file within your server's plugin dir, or 20 | upload it in the web interface. A server restart is required. 21 | 22 | # Usage 23 | There are two steps to get started using the plugin: 24 | 25 | 1. Create a Cloud profile 26 | 2. Configure a build to run within a container 27 | 28 | ## Cloud profiles 29 | TeamCity has a concept of Cloud profiles, which can start agents on demand. 30 | In this plugin, a Cloud profile is created with a single Container provider, 31 | which is then responsible for starting the containers as requested by TeamCity. 32 | If required, you can create several Cloud profiles using the plugin with 33 | different configurations. 34 | 35 | To create a cloud profile, go to Administration > Cloud agents, then click 36 | "Create new profile". Enter a name for the profile, then select "Container cloud" 37 | from the "Cloud type" drop down. Note that the "Terminate instance idle time" is 38 | ignored, and "Terminate instance" will always be set to "After first build 39 | finished", no matter is configured here. 40 | 41 | Select the desired Container provider and fill in any options required. 42 | 43 | ## Run in Container Cloud build feature 44 | A new build feature "Run in Container Cloud" is included with the plugin, which 45 | allows specifying a profile and image for the build directly from the build 46 | configuration. An image added here is automatically added to the profile's list 47 | of images if not already present. 48 | 49 | ## Container providers 50 | Currently there are two providers supported, described below, Docket socket and 51 | [Helios](https://github.com/spotify/helios), but many more are planned. 52 | 53 | ### Docker socket 54 | This provider connects to a single Docker instance, either over a local unix 55 | socket or using http(s), and runs agents there. Simple to get started, but 56 | limited to as many agents as can be run on a single machine. 57 | 58 | #### Configuration options 59 | `API endpoint`: How to reach the Docker socket. Defaults to using the local 60 | `DOCKER_HOST` environment variable. 61 | 62 | 63 | ### Helios 64 | This provider uses Spotify's Helios orchestration tool to run agents on any 65 | host joined to the Helios cluster. Specifying a subset of hosts is possible 66 | with either host name pattern matching or label selectors. 67 | 68 | Currently, a random agent matching the given criteria will be chosen when 69 | starting a new build agent. 70 | 71 | #### Configuration options 72 | `Master url`: Url a single master, or the SRV record designating the a cluster. 73 | Required. 74 | 75 | `Host name pattern`: Substring pattern agent host names must match. For example 76 | given agents `foo.mydomain` and `foobar.mydomain`, a pattern of `foo` would 77 | match both agents, while the pattern `bar` would only match the second one. 78 | Optional, if not given, all agents will be matched. 79 | 80 | `Host selectors`: Comma-separated list of selectors used to filter agents. For 81 | example, `role=builder,az=us-east-1` would select only agents with both those 82 | labels set. 83 | Optional, if not given, all agents will be matched. 84 | 85 | ## Building only in a specific container image or profile 86 | TeamCity will compute which agents can process a certain build and then choose 87 | one of those. If you want to make sure a build is only run with a specific 88 | container image, add an Agent requirement with 89 | `env.CONTAINER_CLOUD_AGENT_IMAGE_ID` equalling the desired image. 90 | 91 | In the same way, to restrict a build to a certain profile, add a requirement 92 | for `system.cloud.profile_id`. 93 | 94 | # Building 95 | The plugin is built using Maven. There are a few dependencies that are not part 96 | of the Teamcity open apis which you will need to aquire from a TeamCity 97 | installation first. An up-to-date list is documented in `pom.xml`, but one 98 | example is `cloud-server.jar`. This is located under 99 | `webapps/ROOT/WEB-INF/lib/cloud-server.jar` in the server installation. 100 | Copy this file to the Maven local repository, including the version number: 101 | `.m2/repository/org/jetbrains/teamcity/cloud-server/10.0/cloud-server-10.0.jar` 102 | 103 | Once you're setup, simply compile and package: 104 | 105 | ```bash 106 | $ mvn clean compile package 107 | ``` 108 | 109 | If all works, the finished plugin is located in `target/container-cloud.zip`. -------------------------------------------------------------------------------- /container-cloud/container-cloud-agent/container-cloud-agent.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /container-cloud/build/build.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/ContainerCloudProfileController.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import jetbrains.buildServer.clouds.CloudClientParameters; 5 | import jetbrains.buildServer.controllers.BaseFormXmlController; 6 | import jetbrains.buildServer.controllers.BasePropertiesBean; 7 | import jetbrains.buildServer.log.Loggers; 8 | import jetbrains.buildServer.serverSide.SBuildServer; 9 | import jetbrains.buildServer.serverSide.crypt.RSACipher; 10 | import jetbrains.buildServer.web.openapi.PluginDescriptor; 11 | import jetbrains.buildServer.web.openapi.WebControllerManager; 12 | import org.jdom.Element; 13 | import org.jetbrains.annotations.NotNull; 14 | import org.springframework.web.servlet.ModelAndView; 15 | import se.capeit.dev.containercloud.cloud.providers.TestConnectionResult; 16 | import se.capeit.dev.containercloud.cloud.providers.ContainerProvider; 17 | import se.capeit.dev.containercloud.cloud.providers.ContainerProviderFactory; 18 | 19 | import javax.servlet.http.HttpServletRequest; 20 | import javax.servlet.http.HttpServletResponse; 21 | import java.util.Map; 22 | 23 | public class ContainerCloudProfileController extends BaseFormXmlController { 24 | private static final Logger LOG = Loggers.SERVER; // Logger.getInstance(ContainerCloudClient.class.getName()); 25 | 26 | public ContainerCloudProfileController(@NotNull SBuildServer server, 27 | @NotNull PluginDescriptor pluginDescriptor, 28 | @NotNull WebControllerManager webControllerManager) { 29 | super(server); 30 | 31 | webControllerManager.registerController(pluginDescriptor.getPluginResourcesPath(ContainerCloudConstants.ProfileSettingsTestConnectionPath), this); 32 | } 33 | 34 | @Override 35 | protected ModelAndView doGet(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse) { 36 | return null; 37 | } 38 | 39 | @Override 40 | protected void doPost(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Element xmlResponse) { 41 | final BasePropertiesBean propsBean = new BasePropertiesBean(null); 42 | PluginPropertiesUtil.bindPropertiesFromRequest(request, propsBean, true); 43 | 44 | final Map properties = propsBean.getProperties(); 45 | 46 | xmlResponse.addContent(testConnection(properties)); 47 | } 48 | 49 | private Element testConnection(Map properties) { 50 | CloudClientParameters cloudClientParameters = new CloudClientParameters(); 51 | properties.entrySet().forEach(entry -> cloudClientParameters.setParameter(entry.getKey(), entry.getValue())); 52 | 53 | ContainerProvider provider = ContainerProviderFactory.getProvider(cloudClientParameters); 54 | TestConnectionResult testConnectionResult = provider.testConnection(); 55 | 56 | Element results = new Element("testConnectionResults"); 57 | 58 | Element success = new Element("ok"); 59 | success.setText(Boolean.toString(testConnectionResult.isOk())); 60 | results.addContent(success); 61 | 62 | if (!testConnectionResult.getMessages().isEmpty()) { 63 | Element messages = new Element("messages"); 64 | for (TestConnectionResult.Message message : testConnectionResult.getMessages()) { 65 | Element messageElement = new Element("message"); 66 | messageElement.setText(message.getMessage()); 67 | messageElement.setAttribute("level", message.getLevel().name()); 68 | messages.addContent(messageElement); 69 | } 70 | results.addContent(messages); 71 | } 72 | 73 | return results; 74 | } 75 | } 76 | 77 | // Copied from https://github.com/JetBrains/teamcity-azure-plugin/blob/master/plugin-azure-server-base/src/main/java/jetbrains/buildServer/clouds/azure/utils/PluginPropertiesUtil.java 78 | class PluginPropertiesUtil { 79 | private final static String PROPERTY_PREFIX = "prop:"; 80 | private static final String ENCRYPTED_PROPERTY_PREFIX = "prop:encrypted:"; 81 | 82 | private PluginPropertiesUtil() { 83 | } 84 | 85 | public static void bindPropertiesFromRequest(HttpServletRequest request, BasePropertiesBean bean) { 86 | bindPropertiesFromRequest(request, bean, false); 87 | } 88 | 89 | static void bindPropertiesFromRequest(HttpServletRequest request, BasePropertiesBean bean, boolean includeEmptyValues) { 90 | bean.clearProperties(); 91 | 92 | for (final Object o : request.getParameterMap().keySet()) { 93 | String paramName = (String) o; 94 | if (paramName.startsWith(PROPERTY_PREFIX)) { 95 | if (paramName.startsWith(ENCRYPTED_PROPERTY_PREFIX)) { 96 | setEncryptedProperty(paramName, request, bean, includeEmptyValues); 97 | } else { 98 | setStringProperty(paramName, request, bean, includeEmptyValues); 99 | } 100 | } 101 | } 102 | } 103 | 104 | private static void setStringProperty(final String paramName, final HttpServletRequest request, 105 | final BasePropertiesBean bean, final boolean includeEmptyValues) { 106 | String propName = paramName.substring(PROPERTY_PREFIX.length()); 107 | final String propertyValue = request.getParameter(paramName).trim(); 108 | if (includeEmptyValues || propertyValue.length() > 0) { 109 | bean.setProperty(propName, toUnixLineFeeds(propertyValue)); 110 | } 111 | } 112 | 113 | private static void setEncryptedProperty(final String paramName, final HttpServletRequest request, 114 | final BasePropertiesBean bean, final boolean includeEmptyValues) { 115 | String propName = paramName.substring(ENCRYPTED_PROPERTY_PREFIX.length()); 116 | String propertyValue = RSACipher.decryptWebRequestData(request.getParameter(paramName)); 117 | if (propertyValue != null && (includeEmptyValues || propertyValue.length() > 0)) { 118 | bean.setProperty(propName, toUnixLineFeeds(propertyValue)); 119 | } 120 | } 121 | 122 | private static String toUnixLineFeeds(final String str) { 123 | return str.replace("\r", ""); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/providers/DockerSocketContainerProvider.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud.providers; 2 | 3 | import com.google.common.base.Strings; 4 | import com.intellij.openapi.diagnostic.Logger; 5 | import com.intellij.openapi.util.text.StringUtil; 6 | import com.spotify.docker.client.DefaultDockerClient; 7 | import com.spotify.docker.client.DockerClient; 8 | import com.spotify.docker.client.exceptions.ContainerNotFoundException; 9 | import com.spotify.docker.client.exceptions.DockerCertificateException; 10 | import com.spotify.docker.client.exceptions.DockerException; 11 | import com.spotify.docker.client.messages.AuthConfig; 12 | import com.spotify.docker.client.messages.ContainerConfig; 13 | import com.spotify.docker.client.messages.ContainerCreation; 14 | import com.spotify.docker.client.messages.ContainerState; 15 | import jetbrains.buildServer.clouds.CloudClientParameters; 16 | import jetbrains.buildServer.clouds.CloudException; 17 | import jetbrains.buildServer.clouds.CloudInstanceUserData; 18 | import jetbrains.buildServer.clouds.InstanceStatus; 19 | import jetbrains.buildServer.log.Loggers; 20 | import jetbrains.buildServer.serverSide.InvalidProperty; 21 | import jetbrains.buildServer.serverSide.PropertiesProcessor; 22 | import org.jetbrains.annotations.NotNull; 23 | import se.capeit.dev.containercloud.cloud.ContainerCloudConstants; 24 | import se.capeit.dev.containercloud.cloud.ContainerCloudImage; 25 | import se.capeit.dev.containercloud.cloud.ContainerCloudInstance; 26 | 27 | import java.io.IOException; 28 | import java.util.ArrayList; 29 | import java.util.Date; 30 | import java.util.List; 31 | 32 | public class DockerSocketContainerProvider implements ContainerProvider, ContainerInstanceInfoProvider { 33 | private static final Logger LOG = Loggers.SERVER; // Logger.getInstance(ContainerCloudImage.class.getName()); 34 | private static final int CONTAINER_STOP_TIMEOUT_SECONDS = 10; 35 | 36 | private final DockerClient dockerClient; 37 | 38 | public DockerSocketContainerProvider(CloudClientParameters cloudClientParams) { 39 | try { 40 | DefaultDockerClient.Builder builder = DefaultDockerClient.fromEnv(); 41 | try { 42 | builder.authConfig(AuthConfig.fromDockerConfig().build()); 43 | } catch(IOException e) { 44 | LOG.info("Could not load docker configuration file, proceeding without authentication settings."); 45 | } 46 | 47 | String apiEndpoint = cloudClientParams.getParameter(ContainerCloudConstants.ProfileParameterName_DockerSocket_Endpoint); 48 | if (!Strings.isNullOrEmpty(apiEndpoint)) { 49 | builder.uri(apiEndpoint); 50 | } 51 | 52 | dockerClient = builder.build(); 53 | } catch (DockerCertificateException e) { 54 | throw new CloudException("Failed to create docker client", e); 55 | } 56 | } 57 | 58 | @Override 59 | public ContainerCloudInstance startInstance(@NotNull String instanceId, @NotNull ContainerCloudImage image, @NotNull CloudInstanceUserData tag) { 60 | try { 61 | LOG.debug("Pulling image " + image.getId()); 62 | dockerClient.pull(image.getId()); 63 | 64 | List environment = new ArrayList<>(); 65 | tag.getCustomAgentConfigurationParameters().forEach((key, value) -> environment.add(key + "=" + value)); 66 | 67 | ContainerConfig cfg = ContainerConfig.builder() 68 | .image(image.getId()) 69 | .env(environment) 70 | .build(); 71 | ContainerCreation creation = dockerClient.createContainer(cfg, instanceId); 72 | LOG.debug("Starting image " + image.getId()); 73 | dockerClient.startContainer(creation.id()); 74 | 75 | return new ContainerCloudInstance(instanceId, image, this); 76 | } catch (Exception e) { 77 | throw new CloudException("Failed to start instance of image " + image.getId(), e); 78 | } 79 | } 80 | 81 | @Override 82 | public void stopInstance(@NotNull ContainerCloudInstance instance) { 83 | try { 84 | LOG.debug("Stopping container " + instance.getInstanceId()); 85 | dockerClient.stopContainer(instance.getInstanceId(), CONTAINER_STOP_TIMEOUT_SECONDS); 86 | } catch (InterruptedException | DockerException e) { 87 | throw new CloudException("Failed to stop instance " + instance.getInstanceId(), e); 88 | } 89 | } 90 | 91 | @Override 92 | public String getError(String instanceId) { 93 | try { 94 | return dockerClient.inspectContainer(instanceId).state().error(); 95 | } catch (ContainerNotFoundException e) { 96 | return null; 97 | } catch (InterruptedException | DockerException e) { 98 | throw new CloudException("Could not get error for container " + instanceId, e); 99 | 100 | } 101 | } 102 | 103 | @Override 104 | public String getNetworkIdentity(String instanceId) { 105 | try { 106 | // TODO: Can this be made better? What if the container has multiple interfaces? 107 | return dockerClient.inspectContainer(instanceId).networkSettings().ipAddress(); 108 | } catch (Exception e) { 109 | LOG.error("Could not determine container ip", e); 110 | return null; 111 | } 112 | } 113 | 114 | @Override 115 | public Date getStartedTime(String instanceId) { 116 | try { 117 | return dockerClient.inspectContainer(instanceId).created(); 118 | } catch (Exception e) { 119 | throw new CloudException("Could not determine container start time", e); 120 | } 121 | } 122 | 123 | @Override 124 | public InstanceStatus getStatus(String instanceId) { 125 | ContainerState state; 126 | try { 127 | state = dockerClient.inspectContainer(instanceId).state(); 128 | } catch (Exception e) { 129 | LOG.error("Cannot get state of container " + instanceId, e); 130 | return InstanceStatus.UNKNOWN; 131 | } 132 | 133 | if (state.running()) 134 | return InstanceStatus.RUNNING; 135 | if (state.restarting()) 136 | return InstanceStatus.RESTARTING; 137 | if (state.oomKilled() || StringUtil.isNotEmpty(state.error())) 138 | return InstanceStatus.ERROR; 139 | if (state.finishedAt() != null) 140 | return InstanceStatus.STOPPED; 141 | 142 | LOG.warn("Could not map state '" + state.toString() + "' to InstanceStatus"); 143 | return InstanceStatus.UNKNOWN; 144 | } 145 | 146 | @NotNull 147 | public static PropertiesProcessor getPropertiesProcessor() { 148 | return properties -> { 149 | ArrayList toReturn = new ArrayList<>(); 150 | 151 | return toReturn; 152 | }; 153 | } 154 | 155 | @Override 156 | public TestConnectionResult testConnection() { 157 | TestConnectionResult result = new TestConnectionResult(); 158 | 159 | try { 160 | String id = dockerClient.info().id(); 161 | //result.addMessage("Successfully connected to Docker instance with id " + id, TestConnectionResult.Message.MessageLevel.INFO); 162 | result.setOk(true); 163 | } catch (DockerException | InterruptedException e) { 164 | result.setOk(false); 165 | result.addMessage("Failed to connect to Docker: " + e.getMessage(), TestConnectionResult.Message.MessageLevel.ERROR); 166 | } 167 | 168 | return result; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/ContainerCloudClient.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud; 2 | 3 | import com.google.common.base.Strings; 4 | import com.intellij.openapi.diagnostic.Logger; 5 | import jetbrains.buildServer.clouds.*; 6 | import jetbrains.buildServer.log.Loggers; 7 | import jetbrains.buildServer.serverSide.AgentDescription; 8 | import org.jetbrains.annotations.NotNull; 9 | import se.capeit.dev.containercloud.cloud.providers.ContainerProvider; 10 | import se.capeit.dev.containercloud.cloud.providers.ContainerProviderFactory; 11 | 12 | import java.util.Collection; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.stream.Collectors; 17 | 18 | public class ContainerCloudClient implements CloudClientEx { 19 | private static final Logger LOG = Loggers.SERVER; // Logger.getInstance(ContainerCloudClient.class.getName()); 20 | 21 | private final CloudState state; 22 | private final Map images; 23 | private final CloudClientParameters cloudClientParams; 24 | private final ContainerProvider containerProvider; 25 | private CloudErrorInfo lastError; 26 | private boolean canCreateContainers; 27 | 28 | public ContainerCloudClient(CloudState state, CloudClientParameters params) { 29 | LOG.info("Creating container client for profile " + state.getProfileId()); 30 | 31 | this.cloudClientParams = params; 32 | this.state = state; 33 | this.canCreateContainers = true; 34 | this.lastError = null; 35 | 36 | this.images = loadImagesFromProfileParameters(); 37 | this.containerProvider = ContainerProviderFactory.getProvider(cloudClientParams); 38 | } 39 | 40 | private Map loadImagesFromProfileParameters() { 41 | String imagesJson = cloudClientParams.getParameter(ContainerCloudConstants.ProfileParameterName_Images); 42 | if (Strings.isNullOrEmpty(imagesJson)) { 43 | return new HashMap<>(); 44 | } 45 | return CloudImageParameters.collectionFromJson(imagesJson).stream() 46 | .map(CloudImageParameters::getId) 47 | .collect(Collectors.toMap(id -> id, ContainerCloudImage::new)); 48 | } 49 | 50 | private void saveImagesToProfileParameters() { 51 | List cloudImageParameters = images.values().stream() 52 | .map(image -> { 53 | CloudImageParameters cip = new CloudImageParameters(); 54 | cip.setParameter(CloudImageParameters.SOURCE_ID_FIELD, image.getId()); 55 | return cip; 56 | }) 57 | .collect(Collectors.toList()); 58 | cloudClientParams.setParameter(ContainerCloudConstants.ProfileParameterName_Images, CloudImageParameters.collectionToJson(cloudImageParameters)); 59 | } 60 | 61 | public synchronized void addImage(String containerImageId) { 62 | if (images.containsKey(containerImageId)) { 63 | LOG.debug("Image " + containerImageId + " is already present in profile " + state.getProfileId()); 64 | return; 65 | } 66 | 67 | // Add to active list of images 68 | images.put(containerImageId, new ContainerCloudImage(containerImageId)); 69 | // Add to profile itself so image is still available if server is restarted 70 | saveImagesToProfileParameters(); 71 | 72 | LOG.debug("Added " + containerImageId + " to profile " + state.getProfileId()); 73 | } 74 | 75 | // CloudClient 76 | public String generateAgentName(@NotNull AgentDescription desc) { 77 | LOG.info("generateAgentName"); 78 | return "TODO-container-agent-name"; 79 | } 80 | 81 | /* Call this method to check if it is possible (in theory) to start new instance of a given image in this profile. */ 82 | public boolean canStartNewInstance(@NotNull CloudImage image) { 83 | // TODO: Validate that the image is in the list of images 84 | return canCreateContainers; 85 | } 86 | 87 | /* Looks for an image with the specified identifier and returns its handle. */ 88 | public CloudImage findImageById(@NotNull String imageId) { 89 | return images.getOrDefault(imageId, null); 90 | } 91 | 92 | /* Checks if the agent is an instance of one of the running instances of that cloud profile. */ 93 | public CloudInstance findInstanceByAgent(@NotNull AgentDescription agent) { 94 | String imageId = agent.getAvailableParameters().get("env." + ContainerCloudConstants.AgentEnvParameterName_ImageId); 95 | if (Strings.isNullOrEmpty(imageId)) { 96 | return null; 97 | } 98 | 99 | CloudImage image = findImageById(imageId); 100 | if (image == null) { 101 | return null; 102 | } 103 | 104 | String instanceId = agent.getAvailableParameters().get("env." + ContainerCloudConstants.AgentEnvParameterName_InstanceId); 105 | if (Strings.isNullOrEmpty(instanceId)) { 106 | return null; 107 | } 108 | 109 | return image.findInstanceById(instanceId); 110 | } 111 | 112 | /* Returns correct error info if there was any or null. */ 113 | public CloudErrorInfo getErrorInfo() { 114 | return lastError; 115 | } 116 | 117 | /* Lists all user selected images. */ 118 | @NotNull 119 | public Collection getImages() { 120 | //LOG.info("Get images: " + images.keySet().stream().collect(Collectors.joining(","))); 121 | return images.values(); 122 | } 123 | 124 | /* Checks if the client data is fully ready to be queried by the system. */ 125 | public boolean isInitialized() { 126 | return true; 127 | } 128 | 129 | // CloudClientEx 130 | /* Notifies client that it is no longer needed, This is a good time to release all resources allocated to implement the client */ 131 | public void dispose() { 132 | LOG.info("Disposing ContainerCloudClient"); 133 | canCreateContainers = false; 134 | //images.values().forEach(ContainerCloudImage::dispose); 135 | } 136 | 137 | /* Restarts instance if possible */ 138 | public void restartInstance(@NotNull CloudInstance instance) { 139 | throw new UnsupportedOperationException("Restart not supported"); 140 | } 141 | 142 | /* Starts a new agent */ 143 | @NotNull 144 | public CloudInstance startNewInstance(@NotNull CloudImage image, @NotNull CloudInstanceUserData tag) { 145 | if (!canCreateContainers) { 146 | LOG.error("Cannot create new container of image " + image.getId() + ", disposing"); 147 | return null; 148 | } 149 | 150 | ContainerCloudImage containerImage = image instanceof ContainerCloudImage ? (ContainerCloudImage) image : null; 151 | if (containerImage == null) { 152 | throw new CloudException("Cannot start instance with image " + image.getId() + ", not a ContainerCloudImage object"); 153 | } 154 | 155 | try { 156 | String instanceId = generateInstanceId(image); 157 | tag.addAgentConfigurationParameter("SERVER_URL", tag.getServerAddress()); 158 | tag.addAgentConfigurationParameter("AGENT_NAME", "container-cloud_" + instanceId); 159 | tag.addAgentConfigurationParameter(ContainerCloudConstants.AgentEnvParameterName_ImageId, image.getId()); 160 | tag.addAgentConfigurationParameter(ContainerCloudConstants.AgentEnvParameterName_InstanceId, instanceId); 161 | 162 | ContainerCloudInstance instance = containerProvider.startInstance(instanceId, containerImage, tag); 163 | // TODO: Should there be some instance/image mapping registry instead? 164 | containerImage.registerInstance(instance); 165 | state.registerRunningInstance(instance.getImageId(), instance.getInstanceId()); 166 | return instance; 167 | } catch (Exception e) { 168 | LOG.error("Failed to start new ContainerCloudInstance: " + e.getMessage(), e); 169 | lastError = new CloudErrorInfo(e.getMessage(), e.getMessage(), e); 170 | throw new CloudException(e.getMessage(), e); 171 | } 172 | } 173 | 174 | @NotNull 175 | private String generateInstanceId(@NotNull CloudImage image) { 176 | return image.getId().replace('/', '_').replace(':', '_').replace('.', '_') + "_" + System.currentTimeMillis(); 177 | } 178 | 179 | /* Terminates instance. */ 180 | public void terminateInstance(@NotNull CloudInstance instance) { 181 | LOG.info("terminateInstance " + instance.getImageId()); 182 | CloudImage image = instance.getImage(); 183 | 184 | ContainerCloudImage cloudImage = image instanceof ContainerCloudImage ? (ContainerCloudImage) image : null; 185 | if (cloudImage == null) { 186 | LOG.error("Cannot stop instance with id " + instance.getInstanceId() + ", not does not have a ContainerCloudImage"); 187 | return; 188 | } 189 | ContainerCloudInstance cloudInstance = instance instanceof ContainerCloudInstance ? (ContainerCloudInstance) instance : null; 190 | 191 | try { 192 | containerProvider.stopInstance(cloudInstance); 193 | cloudImage.unregisterInstance(cloudInstance.getInstanceId()); 194 | } catch (Exception e) { 195 | LOG.error("Failed to stop ContainerCloudInstance " + instance.getInstanceId(), e); 196 | lastError = new CloudErrorInfo(e.getMessage(), e.getMessage(), e); 197 | } 198 | state.registerTerminatedInstance(image.getId(), instance.getInstanceId()); 199 | } 200 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/resources/buildServerResources/profile-settings.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="props" tagdir="/WEB-INF/tags/props" %> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 3 | <%@ taglib prefix="l" tagdir="/WEB-INF/tags/layout" %> 4 | 5 | 6 | 7 | 8 | 45 | 46 | 47 | 48 | 49 | 53 | Docker socket 54 | Helios 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 64 | 65 | 66 |

Url to the Docker API endpoint, reachable from the Teamcity server. Can be either a unix domain socket (unix:///var/run/docker.sock) or a http(s) socket (https://remote-host).

67 |

If not specified, will use the DOCKER_HOST environment variable.

68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 79 | 80 | 81 | Url to a Helios master, or a Helios cluster 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 92 | 93 | 94 | Host name pattern used to filter hosts, using substring matching. If empty, all hosts are used. 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 105 | 106 | 107 | Label selectors used to filter hosts (eg role=builder). If empty, all hosts are used. 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
118 | 119 | 120 | 121 | Add image 122 | 123 |
124 | 125 | 126 |
127 | 129 | 130 | Remove image(s) 131 | 132 |
133 | 134 | 135 | Images available from this provider. Note that if images are specified in a "Run in Container Cloud" Build 136 | feature for a build configuration, these will be added here as well. 137 | 138 | 139 | 140 | 141 | 142 | Test connection 143 | 144 |
145 | 150 |
151 | 152 | Testing... 153 |
154 |
155 |
156 | 157 | Success! 158 |
159 |
160 | 161 | Connection test was not successful 162 |
163 |
164 |
165 |
166 |
    167 |
    168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/container-cloud-server.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /container-cloud/container-cloud-server/src/main/java/se/capeit/dev/containercloud/cloud/providers/HeliosContainerProvider.java: -------------------------------------------------------------------------------- 1 | package se.capeit.dev.containercloud.cloud.providers; 2 | 3 | import com.google.common.base.Strings; 4 | import com.google.common.util.concurrent.ListenableFuture; 5 | import com.intellij.openapi.diagnostic.Logger; 6 | import com.spotify.helios.client.HeliosClient; 7 | import com.spotify.helios.common.descriptors.*; 8 | import com.spotify.helios.common.protocol.CreateJobResponse; 9 | import com.spotify.helios.common.protocol.JobDeleteResponse; 10 | import com.spotify.helios.common.protocol.JobDeployResponse; 11 | import com.spotify.helios.common.protocol.JobUndeployResponse; 12 | import jetbrains.buildServer.clouds.CloudClientParameters; 13 | import jetbrains.buildServer.clouds.CloudException; 14 | import jetbrains.buildServer.clouds.CloudInstanceUserData; 15 | import jetbrains.buildServer.clouds.InstanceStatus; 16 | import jetbrains.buildServer.log.Loggers; 17 | import jetbrains.buildServer.serverSide.InvalidProperty; 18 | import jetbrains.buildServer.serverSide.PropertiesProcessor; 19 | import org.jetbrains.annotations.NotNull; 20 | import se.capeit.dev.containercloud.cloud.ContainerCloudConstants; 21 | import se.capeit.dev.containercloud.cloud.ContainerCloudImage; 22 | import se.capeit.dev.containercloud.cloud.ContainerCloudInstance; 23 | 24 | import java.util.*; 25 | import java.util.concurrent.ConcurrentHashMap; 26 | import java.util.concurrent.ExecutionException; 27 | import java.util.concurrent.TimeUnit; 28 | import java.util.concurrent.TimeoutException; 29 | import java.util.stream.Collectors; 30 | 31 | public class HeliosContainerProvider implements ContainerProvider, ContainerInstanceInfoProvider { 32 | private static final Logger LOG = Loggers.SERVER; // Logger.getInstance(ContainerCloudInstance.class.getName()); 33 | private static final int HELIOS_OPERATION_TIMEOUT_SECONDS = 5; 34 | 35 | private final HeliosClient heliosClient; 36 | private final CloudClientParameters cloudClientParams; 37 | private final ConcurrentHashMap instanceIdJobMap; 38 | private final Random random = new Random(); 39 | 40 | public HeliosContainerProvider(CloudClientParameters cloudClientParams) { 41 | this.cloudClientParams = cloudClientParams; 42 | this.heliosClient = HeliosClient.newBuilder() 43 | .setUser("teamcity-container-cloud") 44 | .setEndpoints(cloudClientParams.getParameter(ContainerCloudConstants.ProfileParameterName_Helios_MasterUrl)) 45 | .build(); 46 | instanceIdJobMap = new ConcurrentHashMap<>(); 47 | } 48 | 49 | private String getRandomMatchingHost() { 50 | try { 51 | List hosts = getHosts(); 52 | if (hosts.size() == 0) { 53 | throw new CloudException("No Helios hosts matched conditions set in cloud profile!"); 54 | } 55 | 56 | int idx = random.nextInt(hosts.size()); 57 | return hosts.get(idx); 58 | } catch (InterruptedException | ExecutionException | TimeoutException e) { 59 | throw new CloudException("Could not get host to run agent on", e); 60 | } 61 | } 62 | 63 | private T getHeliosResult(ListenableFuture future) throws InterruptedException, ExecutionException, TimeoutException { 64 | return future.get(HELIOS_OPERATION_TIMEOUT_SECONDS, TimeUnit.SECONDS); 65 | } 66 | 67 | private List getHosts() throws InterruptedException, ExecutionException, TimeoutException { 68 | String namePattern = cloudClientParams.getParameter(ContainerCloudConstants.ProfileParameterName_Helios_HostNamePattern); 69 | String selectors = cloudClientParams.getParameter(ContainerCloudConstants.ProfileParameterName_Helios_HostSelectors); 70 | 71 | List hosts; 72 | if (!Strings.isNullOrEmpty(namePattern) && !Strings.isNullOrEmpty(selectors)) { 73 | Set selectorSet = Arrays.stream(selectors.split(",")).collect(Collectors.toSet()); 74 | hosts = getHeliosResult(heliosClient.listHosts(namePattern, selectorSet)); 75 | } else if (!Strings.isNullOrEmpty(namePattern)) { 76 | hosts = getHeliosResult(heliosClient.listHosts(namePattern)); 77 | } else if (!Strings.isNullOrEmpty(selectors)) { 78 | Set selectorSet = Arrays.stream(selectors.split(",")).collect(Collectors.toSet()); 79 | hosts = getHeliosResult(heliosClient.listHosts(selectorSet)); 80 | } else { 81 | hosts = getHeliosResult(heliosClient.listHosts()); 82 | } 83 | return hosts; 84 | } 85 | 86 | @Override 87 | public ContainerCloudInstance startInstance(@NotNull String instanceId, @NotNull ContainerCloudImage image, @NotNull CloudInstanceUserData tag) { 88 | Job.Builder jobBuilder = Job.newBuilder() 89 | .setImage(image.getId()) 90 | .setName("container-cloud-agent") 91 | .setVersion(instanceId); 92 | // Add all tag custom configuration as environment vars 93 | tag.getCustomAgentConfigurationParameters().forEach(jobBuilder::addEnv); 94 | Job jobDescriptor = jobBuilder.build(); 95 | 96 | JobDeployResponse jobDeployResponse; 97 | String id; 98 | try { 99 | CreateJobResponse jobResponse = getHeliosResult(heliosClient.createJob(jobDescriptor)); 100 | id = jobResponse.getId(); 101 | 102 | List jobCreationErrors = jobResponse.getErrors(); 103 | if (!jobCreationErrors.isEmpty()) { 104 | String errorList = jobCreationErrors.stream().collect(Collectors.joining("\n")); 105 | throw new CloudException("Failed to create Helios job, errors were reported:\n" + errorList); 106 | } 107 | if (jobResponse.getStatus() != CreateJobResponse.Status.OK) { 108 | throw new CloudException("Failed to create Helios job, status is '" + jobResponse.getStatus() + "', not 'OK'"); 109 | } 110 | 111 | String host = getRandomMatchingHost(); // TODO: Implement some more intelligent host picking strategy? (look at host stats like memory etc) 112 | LOG.debug("Deploying job " + id + " on host " + host); 113 | jobDeployResponse = getHeliosResult(heliosClient.deploy(Deployment.of(JobId.fromString(id), Goal.START), host)); 114 | } catch (InterruptedException | ExecutionException | TimeoutException e) { 115 | throw new CloudException("Failed to start Helios job", e); 116 | } 117 | 118 | if (jobDeployResponse.getStatus() != JobDeployResponse.Status.OK) { 119 | throw new CloudException("Helios job status is '" + jobDeployResponse.getStatus() + "' not 'OK'"); 120 | } 121 | LOG.debug("Started Helios job " + id); 122 | 123 | ContainerCloudInstance cloudInstance = new ContainerCloudInstance(instanceId, image, this); 124 | instanceIdJobMap.put(instanceId, id); 125 | return cloudInstance; 126 | } 127 | 128 | @Override 129 | public void stopInstance(@NotNull ContainerCloudInstance instance) { 130 | try { 131 | JobId jobId = JobId.fromString(instanceIdJobMap.get(instance.getInstanceId())); 132 | LOG.debug("Stopping Helios job " + jobId); 133 | JobStatus jobStatus = getHeliosResult(heliosClient.jobStatus(jobId)); 134 | Set hosts = jobStatus.getDeployments().keySet(); 135 | 136 | for (String host : hosts) { 137 | LOG.debug("Undeploying " + jobId + " from " + host); 138 | JobUndeployResponse jobUndeployResponse = getHeliosResult(heliosClient.undeploy(jobId, host)); 139 | if (jobUndeployResponse.getStatus() != JobUndeployResponse.Status.OK) { 140 | throw new CloudException("Failed to undeploy Helios job, status " + jobUndeployResponse.getStatus()); 141 | } 142 | } 143 | 144 | LOG.debug("Undeploy finished, deleting job " + jobId); 145 | JobDeleteResponse jobDeleteResponse = getHeliosResult(heliosClient.deleteJob(jobId)); 146 | if (jobDeleteResponse.getStatus() != JobDeleteResponse.Status.OK) { 147 | throw new CloudException("Failed to remove Helios job, status " + jobDeleteResponse.getStatus()); 148 | } 149 | 150 | LOG.debug("Finished stopping instance " + instance.getInstanceId()); 151 | instanceIdJobMap.remove(instance.getInstanceId()); 152 | } catch (InterruptedException | ExecutionException | TimeoutException e) { 153 | throw new CloudException("Failed to stop instance", e); 154 | } 155 | } 156 | 157 | @Override 158 | public String getError(String instanceId) { 159 | try { 160 | JobId jobId = JobId.fromString(instanceIdJobMap.get(instanceId)); 161 | JobStatus jobStatus = getHeliosResult(heliosClient.jobStatus(jobId)); 162 | if (jobStatus == null) { 163 | LOG.warn("Trying to read error state of non-existent job " + jobId); 164 | return null; 165 | } 166 | Set hostStatuses = jobStatus.getTaskStatuses().keySet(); 167 | if (hostStatuses.isEmpty()) { 168 | LOG.debug("Trying to read error state before job is deployed for job " + jobId); 169 | return null; 170 | } 171 | 172 | // There really should only ever be one host that the job is deployed to, but let's be thorough 173 | StringBuilder sb = new StringBuilder(); 174 | for (String host : hostStatuses) { 175 | TaskStatus status = jobStatus.getTaskStatuses().get(host); 176 | if (status.getContainerError() != null && !status.getContainerError().isEmpty()) { 177 | sb.append(host).append(": ").append(status.getContainerError()).append("\n"); 178 | } 179 | } 180 | return sb.toString(); 181 | } catch (InterruptedException | ExecutionException | TimeoutException e) { 182 | throw new CloudException("Failed to read error information from instance " + instanceId, e); 183 | } 184 | } 185 | 186 | @Override 187 | public String getNetworkIdentity(String instanceId) { 188 | try { 189 | JobId jobId = JobId.fromString(instanceIdJobMap.get(instanceId)); 190 | JobStatus jobStatus = getHeliosResult(heliosClient.jobStatus(jobId)); 191 | if (jobStatus == null) { 192 | LOG.warn("Trying to read network identity of non-existent job " + jobId.getName()); 193 | return null; 194 | } 195 | 196 | String host = jobStatus.getTaskStatuses().keySet().stream() 197 | .findFirst() 198 | .orElseThrow(() -> new CloudException("Container " + instanceId + " not deployed on any host")); 199 | // TODO: How to do this? 200 | return null; 201 | } catch (InterruptedException | ExecutionException | TimeoutException e) { 202 | throw new CloudException("Failed to read network information from instance " + instanceId, e); 203 | } 204 | } 205 | 206 | @Override 207 | public Date getStartedTime(String instanceId) { 208 | try { 209 | JobId jobId = JobId.fromString(instanceIdJobMap.get(instanceId)); 210 | JobStatus jobStatus = getHeliosResult(heliosClient.jobStatus(jobId)); 211 | if (jobStatus == null) { 212 | LOG.warn("Trying to read start time of non-existent job " + jobId.getName()); 213 | return null; 214 | } 215 | 216 | String host = jobStatus.getTaskStatuses().keySet().stream() 217 | .findFirst() 218 | .orElseThrow(() -> new CloudException("Container " + instanceId + " not deployed on any host")); 219 | return new Date(jobStatus.getTaskStatuses().get(host).getJob().getCreated()); 220 | } catch (InterruptedException | ExecutionException | TimeoutException e) { 221 | throw new CloudException("Failed to read start time information from instance " + instanceId, e); 222 | } 223 | } 224 | 225 | @Override 226 | public InstanceStatus getStatus(String instanceId) { 227 | try { 228 | JobId jobId = JobId.fromString(instanceIdJobMap.get(instanceId)); 229 | JobStatus jobStatus = getHeliosResult(heliosClient.jobStatus(jobId)); 230 | if (jobStatus == null) { 231 | LOG.warn("Trying to read status of non-existent job " + jobId.getName()); 232 | return InstanceStatus.UNKNOWN; 233 | } 234 | 235 | String host = jobStatus.getTaskStatuses().keySet().stream() 236 | .findFirst() 237 | .orElseThrow(() -> new CloudException("Container " + instanceId + " not deployed on any host")); 238 | TaskStatus.State state = jobStatus.getTaskStatuses().get(host).getState(); 239 | 240 | if (state.equals(TaskStatus.State.RUNNING)) 241 | return InstanceStatus.RUNNING; 242 | if (state.equals(TaskStatus.State.STOPPED) || state.equals(TaskStatus.State.EXITED)) 243 | return InstanceStatus.STOPPED; 244 | if (state.equals(TaskStatus.State.FAILED)) 245 | return InstanceStatus.ERROR; 246 | if (state.equals(TaskStatus.State.STARTING) || state.equals(TaskStatus.State.CREATING) || state.equals(TaskStatus.State.PULLING_IMAGE)) 247 | return InstanceStatus.STARTING; 248 | if (state.equals(TaskStatus.State.STOPPING)) 249 | return InstanceStatus.STOPPING; 250 | 251 | LOG.warn("Could not map state '" + state.toString() + "' to InstanceStatus"); 252 | return InstanceStatus.UNKNOWN; 253 | } catch (InterruptedException | ExecutionException | TimeoutException e) { 254 | LOG.warn("Could not fetch state for " + instanceId, e); 255 | return InstanceStatus.UNKNOWN; 256 | } 257 | } 258 | 259 | public static PropertiesProcessor getPropertiesProcessor() { 260 | return properties -> { 261 | ArrayList toReturn = new ArrayList<>(); 262 | if (!properties.containsKey(ContainerCloudConstants.ProfileParameterName_Helios_MasterUrl) || 263 | properties.get(ContainerCloudConstants.ProfileParameterName_Helios_MasterUrl).isEmpty()) 264 | toReturn.add(new InvalidProperty(ContainerCloudConstants.ProfileParameterName_Helios_MasterUrl, 265 | "Helios master Url is required")); 266 | 267 | return toReturn; 268 | }; 269 | } 270 | 271 | @Override 272 | public TestConnectionResult testConnection() { 273 | TestConnectionResult result = new TestConnectionResult(); 274 | try { 275 | if (getHosts().isEmpty()) { 276 | result.addMessage("Connection successful, but no hosts matched name and label conditions. Will not be able to start containers.", TestConnectionResult.Message.MessageLevel.WARNING); 277 | } 278 | 279 | result.setOk(true); 280 | return result; 281 | } catch(Exception e) { 282 | result.setOk(false); 283 | result.addMessage("Failed to connect to Helios: " + e.getMessage(), TestConnectionResult.Message.MessageLevel.ERROR); 284 | return result; 285 | } 286 | } 287 | } 288 | --------------------------------------------------------------------------------