diff --git a/README.md b/README.md index ec6eb5e9..d4003d13 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,49 @@ thereby keeping configuration more [DRY](http://en.wikipedia.org/wiki/Don%27t_re SeeĀ [Docker Pipeline Plugin](https://plugins.jenkins.io/docker-workflow) for the typical usage. +### Multiple Private/Authenticated Registries + +In some scenarios you need to authenticate between multiple registries from within a single pipeline step/command. Below +are some examples to allow this: + +*Scripted* +```groovy +node('docker') { + docker.withRegistry('private-repo1-url', 'id-for-a-docker-cred-repo1') { + docker.withRegistry('private-repo2-url', 'id-for-a-docker-cred-repo2') { + writeFile file: 'Dockerfile', text: ''' + FROM private-repo1-url/image + COPY someFile / + ENTRYPOINT /someFile''' + sh 'docker build --tag private-repo2-url/myapp .' + } + } +} +``` + +*Declarative* +```groovy +pipeline { + agent docker + stages { + stage('foo') { + steps { + docker.withRegistry('private-repo1-url', 'id-for-a-docker-cred-repo1') { + docker.withRegistry('private-repo2-url', 'id-for-a-docker-cred-repo2') { + writeFile file: 'Dockerfile', text: ''' + FROM private-repo1-url/image + COPY someFile / + ENTRYPOINT /someFile''' + sh 'docker build --tag private-repo2-url/myapp .' + } + } + } + } + } +} +``` + + ## Declarative pipeline example An example on how to bind Docker host/daemon credentials in a declarative pipeline: diff --git a/src/main/java/org/jenkinsci/plugins/docker/commons/impl/RegistryKeyMaterialFactory.java b/src/main/java/org/jenkinsci/plugins/docker/commons/impl/RegistryKeyMaterialFactory.java index 6478387a..fabb8407 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/commons/impl/RegistryKeyMaterialFactory.java +++ b/src/main/java/org/jenkinsci/plugins/docker/commons/impl/RegistryKeyMaterialFactory.java @@ -24,9 +24,11 @@ package org.jenkinsci.plugins.docker.commons.impl; +import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; import javax.annotation.Nonnull; @@ -51,7 +53,9 @@ public class RegistryKeyMaterialFactory extends KeyMaterialFactory { private static final String DOCKER_CONFIG_FILENAME = "config.json"; - private static final String[] BLACKLISTED_PROPERTIES = { "auths", "credsStore" }; + private static final String BLACKLISTED_PROPERTY_CREDS_STORE = "credsStore"; + private static final String BLACKLISTED_PROPERTY_AUTHS = "auths"; + private static final String[] BLACKLISTED_PROPERTIES = { BLACKLISTED_PROPERTY_AUTHS, BLACKLISTED_PROPERTY_CREDS_STORE }; private final @Nonnull String username; private final @Nonnull String password; @@ -75,23 +79,25 @@ public RegistryKeyMaterialFactory(@Nonnull String username, @Nonnull String pass public KeyMaterial materialize() throws IOException, InterruptedException { FilePath dockerConfig = createSecretsDirectory(); - // read the existing docker config file, which might hold some important settings (e.b. proxies) + // read the user's home dir docker config file, which might hold some important settings (e.b. proxies) FilePath configJsonPath = FilePath.getHomeDirectory(this.launcher.getChannel()).child(".docker").child(DOCKER_CONFIG_FILENAME); - if (configJsonPath.exists()) { - String configJson = configJsonPath.readToString(); - if (StringUtils.isNotBlank(configJson)) { - launcher.getListener().getLogger().print("Using the existing docker config file."); - - JSONObject json = JSONObject.fromObject(configJson); - for (String property : BLACKLISTED_PROPERTIES) { - Object value = json.remove(property); - if (value != null) { - launcher.getListener().getLogger().print("Removing blacklisted property: " + property); - } - } + // read the current docker config which might hold some existing settings (e.b. credentials) + FilePath existingDockerConfigPath = new FilePath(this.launcher.getChannel(), + Paths.get(this.env.get("DOCKER_CONFIG"), DOCKER_CONFIG_FILENAME).toString()); + + String dockerConfigJson = ""; + if (existingDockerConfigPath.exists()) { + this.launcher.getListener().getLogger().print("Reading the existing DOCKER_CONFIG '" + + existingDockerConfigPath + "' docker config file.\n"); + dockerConfigJson = existingDockerConfigPath.readToString(); + } else if (configJsonPath.exists()) { + this.launcher.getListener().getLogger().print("Reading the existing user's home '" + + configJsonPath + "' docker config file.\n"); + dockerConfigJson = removeBlacklistedProperties(configJsonPath.readToString(), BLACKLISTED_PROPERTIES); + } - dockerConfig.child(DOCKER_CONFIG_FILENAME).write(json.toString(), StandardCharsets.UTF_8.name()); - } + if (StringUtils.isNotBlank(dockerConfigJson)) { + dockerConfig.child(DOCKER_CONFIG_FILENAME).write(dockerConfigJson, StandardCharsets.UTF_8.name()); } try { @@ -112,6 +118,21 @@ public KeyMaterial materialize() throws IOException, InterruptedException { return new RegistryKeyMaterial(dockerConfig, new EnvVars("DOCKER_CONFIG", dockerConfig.getRemote())); } + private String removeBlacklistedProperties(@Nonnull String json, @Nonnull String[] blacklistedProperties) { + String jsonString = ""; + if (StringUtils.isNotBlank(json)) { + JSONObject jsonObject = JSONObject.fromObject(json); + for (String property : blacklistedProperties) { + Object value = jsonObject.remove(property); + if (value != null) { + this.launcher.getListener().getLogger().print("Removing blacklisted property: " + property + "\n"); + } + } + jsonString = jsonObject.toString(); + } + return jsonString; + } + private static class RegistryKeyMaterial extends KeyMaterial { private final FilePath dockerConfig; diff --git a/src/test/java/org/jenkinsci/plugins/docker/commons/impl/RegistryKeyMaterialFactoryTest.java b/src/test/java/org/jenkinsci/plugins/docker/commons/impl/RegistryKeyMaterialFactoryTest.java index 02f03fc8..d1f42f80 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/commons/impl/RegistryKeyMaterialFactoryTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/commons/impl/RegistryKeyMaterialFactoryTest.java @@ -27,6 +27,7 @@ import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.emptyArray; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -41,6 +42,7 @@ import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterialContext; import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterialFactory; import org.jenkinsci.plugins.docker.commons.tools.DockerTool; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -48,6 +50,7 @@ import org.jvnet.hudson.test.FakeLauncher; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.PretendSlave; +import net.sf.json.JSONObject; import hudson.EnvVars; import hudson.FilePath; @@ -66,6 +69,7 @@ public class RegistryKeyMaterialFactoryTest { @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + private EnvVars env = new EnvVars(); private KeyMaterialFactory factory; @Before @@ -98,13 +102,17 @@ public V call(final Callable callable) throws T { }; URL endpoint = new DockerRegistryEndpoint(null, null).getEffectiveUrl(); - EnvVars env = new EnvVars(); String dockerExecutable = DockerTool.getExecutable(null, null, listener, env); factory = new RegistryKeyMaterialFactory("username", "password", endpoint, launcher, env, listener, dockerExecutable).contextualize(new KeyMaterialContext(new FilePath(tempFolder.newFolder()))); } + @After + public void reset() { + env = new EnvVars(); + } + @Test public void materialize_userConfigFileNotPresent_notCreated() throws Exception { // act @@ -232,4 +240,34 @@ public void materialize_userConfigFileWithCredStoreAndHttpHeaders_createdWithHea assertEquals("{\"HttpHeaders\":{\"User-Agent\":\"Docker-Client\"}}", FileUtils.readFileToString(jsonFile)); } + @Test + public void materialize_existingConfigFile_updatedWithCredentials() throws Exception { + + // arrange + File existingCfgFile = new File(new File(tempFolder.getRoot(), ".docker"), "config.json"); + String existingCfgPath = new File(existingCfgFile.getAbsolutePath()).getParent(); + String updatedConfigJson = "{" + "\"auths\":{\"localhost2:5001\":{\"auth\":\"whatever2\",\"email\":\"\"}}," + + "\"proxies\":{\"default\":{\"httpProxy\":\"proxy\",\"noProxy\":\"something\"}}" + "}"; + FileUtils.write(existingCfgFile, updatedConfigJson); + assertNotNull(existingCfgPath); + env.put("DOCKER_CONFIG", existingCfgPath); + assertNotNull(env.get("DOCKER_CONFIG", null)); + + // act + KeyMaterial material = factory.materialize(); + + // assert + String cfgFolderPath = material.env().get("DOCKER_CONFIG", null); + assertNotNull(cfgFolderPath); + assertNotEquals(cfgFolderPath, existingCfgPath); + + File dockerCfgFolder = new File(cfgFolderPath); + assertTrue(dockerCfgFolder.exists()); + + String[] existingFiles = dockerCfgFolder.list(); + assertThat(existingFiles, arrayContaining("config.json")); + + File jsonFile = new File(dockerCfgFolder, "config.json"); + assertEquals(updatedConfigJson, FileUtils.readFileToString(jsonFile)); + } }