--------------------------------------------------------------------------------
/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 extends CloudInstance> 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 extends CloudImage> 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 |
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 |