diff --git a/README.md b/README.md index b4308e07..be10b0ab 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/src/com/cloudogu/ces/cesbuildlib/Docker.groovy b/src/com/cloudogu/ces/cesbuildlib/Docker.groovy index b8475067..d24399b2 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Docker.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Docker.groovy @@ -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 } @@ -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" } } } \ No newline at end of file diff --git a/test/com/cloudogu/ces/cesbuildlib/DockerTest.groovy b/test/com/cloudogu/ces/cesbuildlib/DockerTest.groovy index 48ad83b3..9d3cc2cf 100644 --- a/test/com/cloudogu/ces/cesbuildlib/DockerTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/DockerTest.groovy @@ -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" @@ -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' } @@ -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' } @@ -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' } @@ -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') @@ -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' } @@ -277,19 +263,12 @@ class DockerTest { } private testExtendedArgs(Closure 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) @@ -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'] @@ -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() @@ -348,7 +324,7 @@ 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:') } @@ -356,7 +332,7 @@ class DockerTest { @Test void imageMountJenkinsUserPasswdEmpty() { actualPasswd = '' - testForInvaildPasswd( + testForInvalidPassword( { image -> image.mountJenkinsUser() }, 'Unable to parse user jenkins from /etc/passwd.') } @@ -364,11 +340,60 @@ class DockerTest { @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 mockedMethod) { Map> mockedScript = [ docker: mockedMethod @@ -376,46 +401,69 @@ class DockerTest { return new Docker(mockedScript) } + /** + * @return Mock Docker instance with mock image, that contains mocked methods. + */ @SuppressWarnings("GroovyAssignabilityCheck") private Docker createWithImage(Map 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 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 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)