Skip to content

Commit 9afc4e7

Browse files
authored
2.1.0 release (#13)
* Major rework of networking, to support multi deployment setups HarborManagerContainer.groovy * Now also modifies the supplied docker compose file to move all containers to a custom network Container.groovy * Some renaming of variables and methods needed, because the groovy autogenerated methods led to confusion and inconsistency. * HarborManagerContainer.groovy * Bug in bash setup script Container.groovy * Bug in status() HarborDeployment.groovy * Improved stopAndRemoveDeployment() so it can stop deployment even if manager container is missing pom.xml * Fixed problems with shading * Untested DockerClientDS.groovy * Extendes the default DockerClientImpl with an exec command that allows a ExecConfig to be supplied Container.groovy * Now uses DockerClientDS * runCommandInContainer now alloes user, group and working dir to be supplied pom.xml * Excluded most/all of groovy from standalone jar to not conflict with the hosting groovy version, consider perhaps instead setting groovy with scope provided * JsmAndBitbucketH2Deployment.groovy * Fixed a boolean logic bug in setupDeployment pom.xml * Added shaded sources * JsmAndBitbucketH2Deployment.groovy * Updated Version of JiraShortcuts used CreateBitbucketLink.groovy * Updated to use the latest JiraShortcuts * pom.xml * Updated Version of JiraShortcuts used * pom.xml * Updated Version of JiraShortcuts used * JsmAndBitbucketH2Deployment.groovy, CreateBitbucketLink.groovy * Tweaks * pom.xml * Bumped jirainstancemanager to 1.3.0 * Bumped maven-shade-plugin to 3.4.1 * AlpineContainer.groovy * Added runCmdAndRm method NginxContainer.groovy * Added setCustomConfig Container.groovy * Added replaceFileInContainer method NginxFileServer.groovy * A new deployment that acts as a simple NGINX file server for anonymous uploads and retrievals Deployment.groovy * Bugfixes in startDeployment * Switched tests to use dockerCertPath "~/.docker/" JsmAndBitbucketH2DeploymentTest.groovy * Bumped ScriptRunner to 7.7.0
1 parent a3fbeb9 commit 9afc4e7

21 files changed

Lines changed: 352 additions & 32 deletions

.github/workflows/publish-maven-package.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ jobs:
5050
run: |
5151
5252
echo Compiling, Packaging and Installing Groovy 2.5 version of library to local m2 directory
53-
mvn install -f pom.xml -DcreateChecksum=true -Dgroovy.major.version=2.5 -Dgroovy.version=2.5.18 -Dspock-core.version=2.2-groovy-2.5 -DjiraShortcuts.version=2.0-SNAPSHOT-groovy-2.5 -Dbitbucketinstancemanager.version=0.0.3-SNAPSHOT-groovy-2.5
53+
mvn install -f pom.xml -DcreateChecksum=true -Dgroovy.major.version=2.5 -Dgroovy.version=2.5.18 -Dspock-core.version=2.2-groovy-2.5 -DjiraShortcuts.version=2.0.1-SNAPSHOT-groovy-2.5 -Dbitbucketinstancemanager.version=0.0.3-SNAPSHOT-groovy-2.5
54+
5455
5556
- name: Copying JAR files
5657
run: |

pom.xml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
</dependency>
4040

4141
<dependency>
42-
<groupId>com.eficode.atlassian</groupId>
43-
<artifactId>JiraShortcuts</artifactId>
42+
<groupId>com.eficode.atlassian.jira</groupId>
43+
<artifactId>jiraShortcuts</artifactId>
4444
<version>${jiraShortcuts.version}</version>
4545
</dependency>
4646

@@ -80,7 +80,7 @@
8080
<dependency>
8181
<groupId>com.eficode.atlassian</groupId>
8282
<artifactId>jirainstancemanager</artifactId>
83-
<version>1.1.0-SNAPSHOT</version>
83+
<version>1.3.0-SNAPSHOT-groovy-${groovy.major.version}</version>
8484
</dependency>
8585

8686

@@ -143,7 +143,7 @@
143143
<!-- Configured in pluginManagement instead of plugins, because we do not want a shaded parent POM -->
144144
<groupId>org.apache.maven.plugins</groupId>
145145
<artifactId>maven-shade-plugin</artifactId>
146-
<version>3.4.0</version>
146+
<version>3.4.1</version>
147147
<executions>
148148
<execution>
149149
<phase>package</phase>
@@ -177,6 +177,7 @@
177177
<exclude>com.google.code.gson:gson</exclude>
178178
<exclude>org.apache.httpcomponents</exclude>
179179
<exclude>commons-*</exclude>
180+
<exclude>com.kohlschutter.junixsocket:junixsocket-core</exclude>
180181

181182
</excludes>
182183

@@ -193,6 +194,7 @@
193194
user would not be able to figure out where the conflicting dependency is. -->
194195
<createDependencyReducedPom>true</createDependencyReducedPom>
195196

197+
<createSourcesJar>true</createSourcesJar>
196198
</configuration>
197199
</execution>
198200
</executions>
@@ -257,7 +259,7 @@
257259
<groovy.major.version>3.0</groovy.major.version>
258260
<groovy.version>3.0.11</groovy.version>
259261
<spock-core.version>2.2-groovy-3.0</spock-core.version>
260-
<jiraShortcuts.version>2.0-SNAPSHOT-groovy-3.0</jiraShortcuts.version>
262+
<jiraShortcuts.version>2.0.1-SNAPSHOT-groovy-3.0</jiraShortcuts.version>
261263
<bitbucketinstancemanager.version>0.0.3-SNAPSHOT-groovy-3.0</bitbucketinstancemanager.version>
262264
</properties>
263265

src/main/groovy/com/eficode/devstack/container/Container.groovy

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,28 @@ trait Container {
602602
}
603603

604604

605+
/**
606+
* Replaces text content of a file in the container
607+
* @param content The new content that should be in the file
608+
* @param filePath Path to the file in the container
609+
* @param verify If true will read back content of file and verify, might five false negatives in case of special chars
610+
* @return
611+
*/
612+
boolean replaceFileInContainer(String content, String filePath, boolean verify = false) {
613+
ArrayList<String> out = runBashCommandInContainer("cat > $filePath <<- 'EOF'\n" + content + "\nEOF")
614+
615+
assert out.isEmpty() : "Unexpected output when replacing file $filePath: " + out.join("\n")
616+
617+
if (verify) {
618+
ArrayList<String>rawOut = runBashCommandInContainer("cat " + filePath)
619+
String readOut = rawOut.join()
620+
assert readOut.trim() == content.trim() : "Error when verifying that the file $filePath was replaced"
621+
return true
622+
}
623+
624+
return true
625+
}
626+
605627
/**
606628
* Copy files from a container
607629
* @param containerPath can be a file or a path (ending in /)

src/main/groovy/com/eficode/devstack/container/impl/AlpineContainer.groovy

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import de.gesellix.docker.client.EngineResponseContent
55
import de.gesellix.docker.remote.api.ContainerCreateRequest
66
import de.gesellix.docker.remote.api.HostConfig
77
import de.gesellix.docker.remote.api.PortBinding
8+
import org.slf4j.Logger
9+
import org.slf4j.LoggerFactory
10+
11+
import java.util.concurrent.TimeoutException
812

913
class AlpineContainer implements Container {
1014

@@ -15,7 +19,6 @@ class AlpineContainer implements Container {
1519
String defaultShell = "/bin/sh"
1620

1721

18-
1922
AlpineContainer(String dockerHost = "", String dockerCertPath = "") {
2023
if (dockerHost && dockerCertPath) {
2124
assert setupSecureRemoteConnection(dockerHost, dockerCertPath): "Error setting up secure remote docker connection"
@@ -26,12 +29,115 @@ class AlpineContainer implements Container {
2629
* Will create an Alpine Container that will sleep indefinitely
2730
* @return
2831
*/
29-
String createSleepyContainer(){
30-
return createContainer([],["tail", "-f", "/dev/null"])
32+
String createSleepyContainer() {
33+
return createContainer([], ["tail", "-f", "/dev/null"])
34+
}
35+
36+
37+
/**
38+
* Creates an Alpine container, runs a command, exits and removes container
39+
* @param cmd A string that will be passed as a command to /bin/sh -c, ex: echo start;sleep 5
40+
* @param timeoutMs
41+
* 0 don't wait, return an array with the container ID immediately,
42+
* timeoutMs > 0 Wait for container to stop, if it takes longer than timeoutMs an exception will be thrown
43+
* @param mounts bind mounts that the container should have:
44+
* readOnly is optional and defaults to true
45+
* ex:[[src: "/tmp/engine/test", target: "/tmp/container/test", readOnly :true]
46+
* @param dockerHost
47+
* @param dockerCertPath
48+
* @return An array of the container logs, or just an array containing container id if timeoutMs == 0
49+
*/
50+
static ArrayList<String> runCmdAndRm(String cmd, long timeoutMs, ArrayList<Map> mounts = [:], String dockerHost = "", String dockerCertPath = "") {
51+
return runCmdAndRm(["/bin/sh", "-c", cmd], timeoutMs, mounts, dockerHost, dockerCertPath)
3152
}
3253

54+
/**
55+
* Creates an Alpine container, runs a command, exits and removes container
56+
* @param cmd An array of commands to run, ex: [ "/bin/sh", "-c", "echo start;sleep 5"]
57+
* @param timeoutMs
58+
* 0 don't wait, return an array with the container ID immediately,
59+
* timeoutMs > 0 Wait for container to stop, if it takes longer than timeoutMs an exception will be thrown
60+
* @param mounts bind mounts that the container should have:
61+
* readOnly is optional and defaults to true
62+
* ex:[[src: "/tmp/engine/test", target: "/tmp/container/test", readOnly :true]
63+
* @param dockerHost
64+
* @param dockerCertPath
65+
* @return An array of the container logs, or just an array containing container id if timeoutMs == 0
66+
*/
67+
static ArrayList<String> runCmdAndRm(ArrayList<String> cmd, long timeoutMs, ArrayList<Map> mounts = [:], String dockerHost = "", String dockerCertPath = "") {
68+
69+
Logger log = LoggerFactory.getLogger(AlpineContainer)
70+
71+
log.info("Running alpine command")
72+
log.info("\tCmd:" + cmd)
73+
AlpineContainer container = null
74+
75+
76+
try {
77+
container = new AlpineContainer(dockerHost, dockerCertPath)
78+
container.containerName = container.containerName + "-cmd-" + System.currentTimeMillis().toString()[-5..-1]
79+
80+
mounts.each {
81+
log.info("\tPreparing Bind mount:")
82+
container.prepareBindMount(it.src as String, it.target as String, it.containsKey("readOnly") ? it.readOnly as Boolean : true)
83+
}
84+
85+
86+
container.createContainer(cmd)
87+
log.info("\tCreated container: " + container.id)
88+
89+
90+
log.info("\tStarted container: " + container.startContainer())
91+
assert !container.hasNeverBeenStarted(): "Error starting Alpine container"
92+
93+
if (timeoutMs == 0) {
94+
log.info("\tNo Timeout set, returning container id")
95+
return [container.id]
96+
}
97+
98+
long start = System.currentTimeMillis()
3399

100+
while (start + timeoutMs > System.currentTimeMillis() && container.running) {
34101

102+
log.info("\tWaited ${System.currentTimeMillis() - start}ms for container to stop")
103+
sleep(1000)
104+
105+
}
106+
log.info("\tContainer finisehd or timed out after ${System.currentTimeMillis() - start}ms")
107+
108+
if (container.running) {
109+
log.info("\t"*2 + "Stopping container forcefully.")
110+
ArrayList<String> containerOut = container.containerLogs
111+
assert container.stopAndRemoveContainer(1): "Error stopping and removing Alpine container after it timed out"
112+
113+
throw new TimeoutException("Alpine container timed out, was forcefully stopped and removed. Container logs:" + containerOut?.join("\n"))
114+
}
115+
116+
117+
118+
ArrayList<String> containerOut = container.containerLogs
119+
120+
log.info("\tReturning ${containerOut.size()} log lines")
121+
122+
assert container.stopAndRemoveContainer(): "Error removing Container:" + container.id
123+
log.info("\tRemoved container:" + container.id)
124+
125+
return containerOut
126+
} catch (ex) {
127+
128+
129+
try {
130+
container.stopAndRemoveContainer(1)
131+
} catch (ignored){}
132+
133+
134+
135+
throw ex
136+
137+
}
138+
139+
140+
}
35141

36142

37143
}

src/main/groovy/com/eficode/devstack/container/impl/NginxContainer.groovy

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class NginxContainer implements Container{
1515
String containerImageTag ="alpine"
1616
String defaultShell = "/bin/sh"
1717

18+
private String customConfigCached
19+
1820

1921

2022
NginxContainer(String dockerHost = "", String dockerCertPath = "") {
@@ -25,7 +27,33 @@ class NginxContainer implements Container{
2527

2628

2729
/**
28-
* Bind local dir to nginx default root dir /usr/share/nginx/html
30+
* Replaces /etc/nginx/conf.d/default.conf and restarts the nginx daemon if needed
31+
* @param customConfig Contents of a new default.conf file
32+
*/
33+
void setCustomConfig(String customConfig) {
34+
customConfigCached = customConfig
35+
36+
if (running) {
37+
replaceFileInContainer(customConfigCached, "/etc/nginx/conf.d/default.conf")
38+
assert runBashCommandInContainer("nginx -s reload")?.first()?.contains("process started") : "Error reloading nginx service after updating config"
39+
}
40+
}
41+
42+
@Override
43+
boolean runOnFirstStartup() {
44+
45+
if (customConfigCached) {
46+
replaceFileInContainer(customConfigCached, "/etc/nginx/conf.d/default.conf")
47+
assert runBashCommandInContainer("nginx -s reload")?.first()?.contains("process started") : "Error reloading nginx service after updating config"
48+
49+
}
50+
51+
return true
52+
}
53+
54+
55+
/**
56+
* Bind docker engine local dir to nginx default root dir /usr/share/nginx/html
2957
* Must be called before creating container
3058
* @param sourceAbs absolut path to local dir
3159
* @param readOnly

src/main/groovy/com/eficode/devstack/deployment/Deployment.groovy

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ trait Deployment {
1919

2020
boolean startDeployment() {
2121

22-
log.info("Starting deployment: " + this.friendlyName)
22+
log.info("Starting deployment: " + this.getFriendlyName())
2323

24-
this.containers.each { container ->
24+
25+
this.getContainers().each { container ->
2526
log.debug("\tStarting:" + container.containerName)
2627
assert container.startContainer(): "Error starting container:" + container.containerId
2728
}

src/main/groovy/com/eficode/devstack/deployment/impl/JsmAndBitbucketH2Deployment.groovy

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ class JsmAndBitbucketH2Deployment implements Deployment {
135135
threadPool.shutdown()
136136

137137

138-
while (!jsmFuture.done && !bitbucketFuture.done) {
138+
while (!jsmFuture.done || !bitbucketFuture.done) {
139139
log.info("Waiting for deployments to finish")
140140
log.info("\tJSM Finished:" + jsmFuture.done)
141141
log.info("\tBitbucket Finished:" + bitbucketFuture.done)
@@ -164,11 +164,12 @@ class JsmAndBitbucketH2Deployment implements Deployment {
164164
log.info("\t\tFinished installing user defined JIRA Apps")
165165
}
166166

167-
if (!jiraAppsToInstall.any { it.key.contains("JiraShortcuts") }) {
167+
if (!jiraAppsToInstall.any { it.key.contains("jiraShortcuts") }) {
168168
log.info("\tInstalling JiraShortcuts app") //Needed for setting up applink to bitbucket
169169
assert installJiraApps(
170170
[
171-
"https://github.com/eficode/JiraShortcuts/raw/packages/repository/com/eficode/atlassian/JiraShortcuts/2.0-SNAPSHOT-groovy-3.0/JiraShortcuts-2.0-SNAPSHOT-groovy-3.0.jar": ""
171+
"https://github.com/eficode/JiraShortcuts/raw/packages/repository/com/eficode/atlassian/jira/jiraShortcuts/2.0.1-SNAPSHOT-groovy-3.0/jiraShortcuts-2.0.1-SNAPSHOT-groovy-3.0.jar":""
172+
172173
]
173174
) : "Error installing JiraShortcuts JIRA apps"
174175
log.info("\t\tFinished installing JiraShortcuts JIRA Apps")
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.eficode.devstack.deployment.impl
2+
3+
import com.eficode.devstack.container.Container
4+
import com.eficode.devstack.container.impl.NginxContainer
5+
import com.eficode.devstack.deployment.Deployment
6+
import org.slf4j.Logger
7+
import org.slf4j.LoggerFactory
8+
9+
/**
10+
* A simple NGINX file server for anonymous uploads and retrievals
11+
*
12+
* Use setPort() to define a custom port, defaults to 80.
13+
*
14+
* The constructor lets you bind a directory on the Docker Engine to the directory used by NGINX for storage
15+
*
16+
*/
17+
18+
class NginxFileServer implements Deployment {
19+
20+
Logger log = LoggerFactory.getLogger(this.class)
21+
String friendlyName = "Nginx File Server"
22+
ArrayList<Container> containers = []
23+
24+
private String getFileServerConfig() {
25+
26+
return """
27+
server {
28+
listen ${container.containerMainPort};
29+
listen [::]:${container.containerMainPort};
30+
server_name localhost;
31+
client_max_body_size 2048M;
32+
33+
location ~ "/([0-9a-zA-Z-.]*)\$" {
34+
alias /usr/share/nginx/html/\$1;
35+
client_body_temp_path /tmp/;
36+
autoindex on;
37+
dav_methods PUT DELETE MKCOL COPY MOVE;
38+
create_full_put_path on;
39+
dav_access group:rw all:r;
40+
}
41+
42+
}
43+
"""
44+
45+
}
46+
47+
NginxContainer getContainer() {
48+
return containers.isEmpty() ? null : containers.first() as NginxContainer
49+
}
50+
51+
void setPort(String port) {
52+
assert !container?.created : "Cant set port on container that has been created"
53+
54+
container.containerMainPort = port
55+
container.setCustomConfig(fileServerConfig ) //Update config
56+
}
57+
58+
/**
59+
* A Simple HTTP file server for anonymous uploads and retrievals
60+
* @param bindToEnginePath A directory on the docker engine where NGINX fill write/get files (Optional)
61+
* @param dockerHost
62+
* @param dockerCertPath
63+
*/
64+
NginxFileServer(String bindToEnginePath = "", String dockerHost = "", String dockerCertPath = "") {
65+
NginxContainer newContainer = new NginxContainer(dockerHost, dockerCertPath)
66+
newContainer.containerName = friendlyName.replace(" ", "-")
67+
bindToEnginePath ? newContainer.bindHtmlRoot(bindToEnginePath, false) : null //Bind Nginx file directory to Docker Engine directory
68+
containers = [newContainer]
69+
70+
//If container already has been created, don't update file on instantiation of new object representation
71+
if (!container.created) {
72+
newContainer.setCustomConfig(fileServerConfig )
73+
}
74+
75+
76+
}
77+
78+
79+
80+
@Override
81+
boolean setupDeployment() {
82+
return containers.first().createContainer() != null
83+
}
84+
}

0 commit comments

Comments
 (0)