Skip to content

Commit

Permalink
Merge pull request #45 from cloudogu/feature/improved_install_docker_…
Browse files Browse the repository at this point in the history
…client

Feature/improved install docker client
  • Loading branch information
pmarkiewka authored Jun 26, 2020
2 parents bd89490 + 00c7547 commit 85f4c02
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 69 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,11 +624,11 @@ Example from Jenkinsfile:

This is also used by [MavenInDocker](src/com/cloudogu/ces/cesbuildlib/MavenInDocker.groovy)

* `installDockerClient(String version)`: Installs the docker client with the specified version inside the container.
* `installDockerClient(String version)`: Installs the docker client with the specified version inside the container.
If no version parameter is passed, the lib tries to query the server version by calling `docker version`.
This can be called in addition to mountDockerSocket(), when the "docker" CLI is required on the PATH.

For available versions see [here](https://download.docker.com/linux/static/stable/x86_64/).
For an exampl see [here](https://github.com/cloudogu/continuous-delivery-docs-example)

Examples:

Expand Down
17 changes: 15 additions & 2 deletions src/com/cloudogu/ces/cesbuildlib/Docker.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,10 @@ class Docker implements Serializable {
*
* For available versions see here: https://download.docker.com/linux/static/stable/x86_64/
*/
Image installDockerClient(String version) {
Image installDockerClient(String version = '') {
if (!version) {
version = sh.returnStdOut "docker version --format '{{.Server.Version}}'"
}
this.dockerClientVersionToInstall = version
return this
}
Expand Down Expand Up @@ -382,7 +385,17 @@ class Docker implements Serializable {

private void doInstallDockerClient() {
// Installs statically linked docker binary
script.sh "cd ${script.pwd()}/.jenkins && wget -qc https://download.docker.com/linux/static/stable/x86_64/docker-$dockerClientVersionToInstall-ce.tgz -O - | tar -xz"
String url = "https://download.docker.com/linux/static/stable/x86_64/docker-${dockerClientVersionToInstall}.tgz"

// Keep compatibility with old URLs
if ((dockerClientVersionToInstall.startsWith('17') ||
dockerClientVersionToInstall.startsWith('18.03') ||
dockerClientVersionToInstall.startsWith('18.06')) &&
!dockerClientVersionToInstall.endsWith('-ce')) {
url = url.replace('.tgz', '-ce.tgz')
}

script.sh "cd ${script.pwd()}/.jenkins && wget -qc ${url} -O - | tar -xz"
}
}
}
178 changes: 113 additions & 65 deletions test/com/cloudogu/ces/cesbuildlib/DockerTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class DockerTest {
def expectedImage = 'google/cloud-sdk:164.0.0'
def expectedHome = '/home/jenkins'
def actualUser = 'jenkins'
def actualDockerServerVersion = '19.03.8'
def actualPasswd = "$actualUser:x:1000:1000:Jenkins,,,:/home/jenkins:/bin/bash"
def actualDockerGroupId = "999"
def actualDockerGroup = "docker:x:$actualDockerGroupId:jenkins"
Expand Down Expand Up @@ -183,10 +184,7 @@ class DockerTest {

@Test
void imageInside() {
Docker docker = createWithImage(
[inside: { String param1, Closure param2 ->
return [param1, param2]
}])
Docker docker = createWithImage(mockedImageMethodInside())

def args = docker.image(expectedImage).inside { return 'expectedClosure' }

Expand All @@ -196,10 +194,7 @@ class DockerTest {

@Test
void imageInsideWithArgs() {
Docker docker = createWithImage(
[inside: { String param1, Closure param2 ->
return [param1, param2]
}])
Docker docker = createWithImage(mockedImageMethodInside())

def args = docker.image(expectedImage).inside('-v a:b') { return 'expectedClosure' }

Expand All @@ -209,10 +204,7 @@ class DockerTest {

@Test
void imageInsideWithEntrypoint() {
Docker docker = createWithImage(
[inside: { String param1, Closure param2 ->
return [param1, param2]
}])
Docker docker = createWithImage(mockedImageMethodInside())

def args = docker.image(expectedImage).inside('--entrypoint="entry"') { return 'expectedClosure' }

Expand Down Expand Up @@ -240,10 +232,7 @@ class DockerTest {

@Test
void imageRun() {
Docker docker = createWithImage(
[run: { String param1, String param2 ->
return [param1, param2]
}])
Docker docker = createWithImage(mockedImageMethodRun())

def args = docker.image(expectedImage).run('arg', 'cmd')

Expand All @@ -253,10 +242,7 @@ class DockerTest {

@Test
void imageWithRun() {
Docker docker = createWithImage(
[withRun: { String param1, String param2, Closure param3 ->
return [param1, param2, param3]
}])
Docker docker = createWithImage(mockedImageMethodWithRun())

def args = docker.image(expectedImage).withRun('arg', 'cmd') { return 'expectedClosure' }

Expand All @@ -277,19 +263,12 @@ class DockerTest {
}

private testExtendedArgs(Closure<Docker.Image> testImage) {
Docker docker = createWithImage(
[inside: { String param1, Closure param2 ->
return [param1, param2]
},
run: { String param1, String param2 ->
return [param1, param2]
}
])
Docker docker = createWithImage(mockedImageMethodInside() + mockedImageMethodRun())

def image = docker.image(expectedImage)
.mountJenkinsUser()
.mountDockerSocket()
.installDockerClient('1.2.3')
.installDockerClient('18.03.1')

def args = testImage.call(image)

Expand All @@ -300,7 +279,7 @@ class DockerTest {

// Docker installed
assert actualShArgs.size() > 0
assert actualShArgs.get(0).contains('https://download.docker.com/linux/static/stable/x86_64/docker-1.2.3-ce.tgz')
assert actualShArgs.get(0).contains('https://download.docker.com/linux/static/stable/x86_64/docker-18.03.1-ce.tgz')

// Written files
assert 'jenkins:x:1000:1000::/home/jenkins:/bin/sh' == actualWriteFileArgs['.jenkins/etc/passwd']
Expand All @@ -322,10 +301,7 @@ class DockerTest {

@Test
void imageWithRunExtendedArgs() {
Docker docker = createWithImage(
[withRun: { String param1, String param2, Closure param3 ->
return [param1, param2, param3]
}])
Docker docker = createWithImage(mockedImageMethodWithRun())

def args = docker.image(expectedImage)
.mountJenkinsUser()
Expand All @@ -348,74 +324,146 @@ class DockerTest {
@Test
void imageMountJenkinsUserUnexpectedPasswd() {
actualPasswd = 'jenkins:x:1000:1000'
testForInvaildPasswd(
testForInvalidPassword(
{ image -> image.mountJenkinsUser() },
'/etc/passwd entry for current user does not match user:x:uid:gid:')
}

@Test
void imageMountJenkinsUserPasswdEmpty() {
actualPasswd = ''
testForInvaildPasswd(
testForInvalidPassword(
{ image -> image.mountJenkinsUser() },
'Unable to parse user jenkins from /etc/passwd.')
}

@Test
void imageMountDockerSocketPasswdEmpty() {
actualDockerGroup = ''
testForInvaildPasswd(
testForInvalidPassword(
{ image -> image.mountDockerSocket() },
'Unable to parse group docker from /etc/group. Docker host will not be accessible for container.')
}

@Test
void "install older docker clients"() {
Docker docker = createWithImage(mockedImageMethodInside())

def oldUrlVersions = [ '17.03.0', '17.03.1', '17.03.2', '17.06.0', '17.06.1', '17.06.2', '17.09.0', '17.09.1',
'17.12.0', '17.12.1', '18.03.0', '18.03.1', '18.06.0', '18.06.1', '18.06.2', '18.06.3']

oldUrlVersions.forEach({
actualShArgs = []
docker.image(expectedImage)
.installDockerClient(it)
.inside { return 'expectedClosure' }
assert actualShArgs[0].contains("${it}-ce.tgz")
})

// Also accept URLs ending in -ce
oldUrlVersions.forEach({
actualShArgs = []
docker.image(expectedImage)
.installDockerClient("${it}-ce")
.inside { return 'expectedClosure' }
assert actualShArgs[0].contains("${it}-ce.tgz")
})
}

@Test
void "install newer docker clients"() {
Docker docker = createWithImage(mockedImageMethodInside())

def oldUrlVersions = [ '18.09.0', '19.03.9']

oldUrlVersions.forEach({
actualShArgs = []
docker.image(expectedImage)
.installDockerClient(it)
.inside { return 'expectedClosure' }
assert actualShArgs[0].contains("${it}.tgz")
})
}

@Test
void "install docker clients for current server version"() {
Docker docker = createWithImage(mockedImageMethodInside())
docker.image(expectedImage)
.installDockerClient()
.inside { return 'expectedClosure' }
assert actualShArgs[0].contains("${actualDockerServerVersion}.tgz")
}

private Docker create(Map<String, Closure> mockedMethod) {
Map<String, Map<String, Closure>> mockedScript = [
docker: mockedMethod
]
return new Docker(mockedScript)
}

/**
* @return Mock Docker instance with mock image, that contains mocked methods.
*/
@SuppressWarnings("GroovyAssignabilityCheck")
private Docker createWithImage(Map<String, Closure> mockedMethod) {

def mockedScript = [
docker: [image: { String id ->
assert id == expectedImage
mockedMethod.put('id', id)
return mockedMethod
}
]
}
],
sh: { args ->

if (!(args instanceof Map)) {
actualShArgs.add(args)
return
}

String script = args['script']
if (script.contains('cat /etc/passwd ')) {
assert script.contains(actualUser)
}
if (script == 'whoami') return actualUser
if (script == 'cat /etc/group | grep docker') return actualDockerGroup
if (script.contains(actualDockerGroup)) return actualDockerGroupId
if (script.contains('cat /etc/passwd | grep')) return actualPasswd
if (script.contains('docker version --format \'{{.Server.Version}}\'')) return " ${actualDockerServerVersion} "
else fail("Unexpected sh call. Script: " + script)
},
pwd: { return expectedHome },
writeFile: { Map<String, String> args -> actualWriteFileArgs.put(args['file'], args['text']) },
error: { String arg -> throw new RuntimeException(arg) }
]
mockedScript.put('sh', { Object args ->

if (!(args instanceof Map)) {
actualShArgs.add(args)
return
}

String script = args['script']
if (script.contains('cat /etc/passwd ')) {
assert script.contains(actualUser)
}
if (script == 'whoami') return actualUser
if (script == 'cat /etc/group | grep docker') return actualDockerGroup
if (script.contains(actualDockerGroup)) return actualDockerGroupId
if (script.contains('cat /etc/passwd | grep')) return actualPasswd
else fail("Unexpected sh call. Script: " + script)
})
mockedScript.put('pwd', { return expectedHome })
mockedScript.put('writeFile', { Map<String, String> args -> actualWriteFileArgs.put(args['file'], args['text']) })
mockedScript.put('error', { String arg -> throw new RuntimeException(arg) })

return new Docker(mockedScript)
}

private void testForInvaildPasswd(Closure imageHook, String expectedError) {
Docker docker = createWithImage(
[run: { String param1, String param2 ->
return [param1, param2]
}])
/**
* @return a map that defines a run() method returning its params, to be used as param in createWithImage()}.
*/
private def mockedImageMethodRun() {
[run: { String param1, String param2 -> return [param1, param2] }]
}

/**
* @return a map that defines an inside() method returning its params, to be used as param in createWithImage()}.
*/
private def mockedImageMethodInside() {
[inside: { String param1, Closure param2 -> return [param1, param2] }]
}

/**
* @return a map that defines a withRun() method returning its params, to be used as param in createWithImage()}.
*/
private def mockedImageMethodWithRun() {
[withRun: { String param1, String param2, Closure param3 ->
return [param1, param2, param3]
}]
}

private void testForInvalidPassword(Closure imageHook, String expectedError) {
Docker docker = createWithImage(mockedImageMethodRun())

def exception = shouldFail {
def image = docker.image(expectedImage)
Expand Down

0 comments on commit 85f4c02

Please sign in to comment.