diff --git a/api-client/build.gradle.kts b/api-client/build.gradle.kts index d408fe79..48b7403e 100644 --- a/api-client/build.gradle.kts +++ b/api-client/build.gradle.kts @@ -3,6 +3,7 @@ import java.util.* plugins { id("java") + id("groovy") id("org.jetbrains.kotlin.jvm") id("maven-publish") id("signing") @@ -76,9 +77,10 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") implementation(libs.moshi) implementation(libs.okhttp) -// implementation("com.squareup.okhttp3:logging-interceptor:[4.9,5)!!4.11.0") + implementation("org.bouncycastle:bcpkix-jdk18on:1.82") + implementation("org.apache.commons:commons-compress:1.28.0") +// implementation("com.squareup.okhttp3:logging-interceptor:${libs.versions.okhttpVersionrange.get()}!!${libs.versions.okhttp.get()}") implementation("de.gesellix:docker-remote-api-model-1-41:2025-10-31T17-49-00") - implementation("de.gesellix:docker-engine:2025-10-31T18-10-00") implementation("de.gesellix:docker-filesocket:2025-10-31T17-48-00") implementation(libs.slf4j) @@ -91,8 +93,11 @@ dependencies { testImplementation(libs.junitPlatformLauncher) testImplementation(libs.junitPlatformCommons) + testImplementation("org.spockframework:spock-core:2.3-groovy-4.0") + testRuntimeOnly("net.bytebuddy:byte-buddy:1.17.8") + testImplementation("org.apache.commons:commons-compress:1.28.0") - testImplementation("de.gesellix:testutil:[2024-01-01T01-01-01,)") + testImplementation("de.gesellix:testutil:[2025-01-01T01-01-01,)") testImplementation("de.gesellix:docker-registry:2025-10-31T17-45-00") } diff --git a/api-client/src/main/java/de/gesellix/docker/authentication/AuthConfig.java b/api-client/src/main/java/de/gesellix/docker/authentication/AuthConfig.java new file mode 100644 index 00000000..68d0949d --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/authentication/AuthConfig.java @@ -0,0 +1,106 @@ +package de.gesellix.docker.authentication; + +import java.util.Objects; + +public class AuthConfig { + + public static final AuthConfig EMPTY_AUTH_CONFIG = new AuthConfig(); + + private String username; + private String password; + private String auth; + /** + * Email is an optional value associated with the username. + * + * @deprecated This field is deprecated and will be removed in a later version of docker. + */ + @Deprecated + private String email; + private String serveraddress; + private String identitytoken; + private String registrytoken; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getAuth() { + return auth; + } + + public void setAuth(String auth) { + this.auth = auth; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getServeraddress() { + return serveraddress; + } + + public void setServeraddress(String serveraddress) { + this.serveraddress = serveraddress; + } + + public String getIdentitytoken() { + return identitytoken; + } + + public void setIdentitytoken(String identitytoken) { + this.identitytoken = identitytoken; + } + + public String getRegistrytoken() { + return registrytoken; + } + + public void setRegistrytoken(String registrytoken) { + this.registrytoken = registrytoken; + } + + @Override + public boolean equals(Object o) { + if (this == o) {return true;} + if (o == null || getClass() != o.getClass()) {return false;} + AuthConfig that = (AuthConfig) o; + return Objects.equals(username, that.username) && Objects.equals(password, that.password) && Objects.equals(auth, that.auth) && + Objects.equals(email, that.email) && Objects.equals(serveraddress, that.serveraddress) && Objects.equals(identitytoken, that.identitytoken) && + Objects.equals(registrytoken, that.registrytoken); + } + + @Override + public int hashCode() { + return Objects.hash(username, password, auth, email, serveraddress, identitytoken, registrytoken); + } + + @Override + public String toString() { + return "AuthConfig{" + + "username='" + username + '\'' + + ", password=_redacted_'" + '\'' + + ", auth=_redacted_'" + '\'' + + ", email='" + email + '\'' + + ", serveraddress='" + serveraddress + '\'' + + ", identitytoken=_redacted_'" + '\'' + + ", registrytoken=_redacted_'" + '\'' + + '}'; + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/authentication/AuthConfigReader.java b/api-client/src/main/java/de/gesellix/docker/authentication/AuthConfigReader.java new file mode 100644 index 00000000..0fc8886b --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/authentication/AuthConfigReader.java @@ -0,0 +1,64 @@ +package de.gesellix.docker.authentication; + +import de.gesellix.docker.engine.DockerConfigReader; +import de.gesellix.docker.engine.DockerEnv; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.Map; + +import static de.gesellix.docker.authentication.AuthConfig.EMPTY_AUTH_CONFIG; + +public class AuthConfigReader { + + private final static Logger log = LoggerFactory.getLogger(AuthConfigReader.class); + + private final DockerEnv env; + private final DockerConfigReader dockerConfigReader; + + public AuthConfigReader() { + this(new DockerEnv()); + } + + public AuthConfigReader(DockerEnv env) { + this.env = env; + this.dockerConfigReader = env.getDockerConfigReader(); + } + + // @Override + public AuthConfig readDefaultAuthConfig() { + return readAuthConfig(null, dockerConfigReader.getDockerConfigFile()); + } + + // @Override + public AuthConfig readAuthConfig(String hostname, File dockerCfg) { + log.debug("read authConfig"); + + if (hostname == null || hostname.trim().isEmpty()) { + hostname = env.getIndexUrl_v1(); + } + + Map parsedDockerCfg = dockerConfigReader.readDockerConfigFile(dockerCfg); + if (parsedDockerCfg == null || parsedDockerCfg.isEmpty()) { + return EMPTY_AUTH_CONFIG; + } + + CredsStore credsStore = getCredentialsStore(parsedDockerCfg, hostname); + return credsStore.getAuthConfig(hostname); + } + + public CredsStore getCredentialsStore(Map parsedDockerCfg) { + return getCredentialsStore(parsedDockerCfg, ""); + } + + public CredsStore getCredentialsStore(Map parsedDockerCfg, String hostname) { + if (parsedDockerCfg.containsKey("credHelpers") && hostname != null && !hostname.trim().isEmpty()) { + return new NativeStore((String) ((Map) parsedDockerCfg.get("credHelpers")).get(hostname)); + } + if (parsedDockerCfg.containsKey("credsStore")) { + return new NativeStore((String) parsedDockerCfg.get("credsStore")); + } + return new FileStore(parsedDockerCfg); + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/authentication/CredsStore.java b/api-client/src/main/java/de/gesellix/docker/authentication/CredsStore.java new file mode 100644 index 00000000..e186a1c9 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/authentication/CredsStore.java @@ -0,0 +1,12 @@ +package de.gesellix.docker.authentication; + +import java.util.Map; + +public interface CredsStore { + + String TOKEN_USERNAME = ""; + + AuthConfig getAuthConfig(String registry); + + Map getAuthConfigs(); +} diff --git a/api-client/src/main/java/de/gesellix/docker/authentication/CredsStoreHelper.java b/api-client/src/main/java/de/gesellix/docker/authentication/CredsStoreHelper.java new file mode 100644 index 00000000..c2622378 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/authentication/CredsStoreHelper.java @@ -0,0 +1,104 @@ +package de.gesellix.docker.authentication; + +import com.squareup.moshi.Moshi; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.text.MessageFormat; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class CredsStoreHelper { + + private static final Logger log = LoggerFactory.getLogger(CredsStoreHelper.class); + private final Moshi moshi = new Moshi.Builder().build(); + + public CredsStoreHelperResult getAuthentication(String credsStore, String hostname) { + Result result = execCredsHelper(credsStore, "get", hostname); + return toCredsStoreHelperResult(result, credsStore); + } + + public CredsStoreHelperResult getAuthentication(String credsStore) { + return getAuthentication(credsStore, "https://index.docker.io/v1/"); + } + + public CredsStoreHelperResult getAllAuthentications(String credsStore) { + Result result = execCredsHelper(credsStore, "list", "unused"); + return toCredsStoreHelperResult(result, credsStore); + } + + public CredsStoreHelperResult toCredsStoreHelperResult(Result result, String credsStore) { + if (!result.getSuccess()) { + return new CredsStoreHelperResult(result.getMessage()); + } + + try { + return new CredsStoreHelperResult(moshi.adapter(Map.class).fromJson(result.getMessage())); + } + catch (IOException exc) { + log.error(MessageFormat.format("cannot parse docker-credential-{0} result", credsStore), exc); + return new CredsStoreHelperResult(exc.getMessage()); + } + catch (Exception exc) { + log.error(MessageFormat.format("error trying to get credentials from docker-credential-{0}", credsStore), exc); + return new CredsStoreHelperResult(exc.getMessage()); + } + } + + private Result execCredsHelper(String credsStore, String command, String input) { + Process process; + try { + process = new ProcessBuilder(MessageFormat.format("docker-credential-{0}", credsStore), command).redirectErrorStream(true).redirectOutput(ProcessBuilder.Redirect.PIPE).start(); + } + catch (Exception exc) { + log.error(MessageFormat.format("error trying to execute docker-credential-{0} {1}", credsStore, command), exc); + return new Result(false, exc.getMessage()); + } + + BufferedReader buffer = new BufferedReader(new InputStreamReader(process.getInputStream())); + + try { + process.getOutputStream().write((input == null ? "".getBytes() : input.getBytes())); + process.getOutputStream().flush(); + process.getOutputStream().close(); + + process.waitFor(10, TimeUnit.SECONDS); + } + catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + + if (process.exitValue() != 0) { + log.error(MessageFormat.format("docker-credential-{0} {1} failed", credsStore, command)); + } + + return new Result(process.exitValue() == 0, buffer.lines().collect(Collectors.joining())); + } + + public static class Result { + + private final boolean success; + private final String message; + + public Result(boolean success, String message) { + this.success = success; + this.message = message; + } + + public boolean getSuccess() { + return success; + } + + public boolean isSuccess() { + return success; + } + + public String getMessage() { + return message; + } + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/authentication/CredsStoreHelperResult.java b/api-client/src/main/java/de/gesellix/docker/authentication/CredsStoreHelperResult.java new file mode 100644 index 00000000..fd8b1721 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/authentication/CredsStoreHelperResult.java @@ -0,0 +1,39 @@ +package de.gesellix.docker.authentication; + +import java.util.Map; +import java.util.Objects; + +public class CredsStoreHelperResult { + + private String error; + private Map data; + + public CredsStoreHelperResult(String error) { + this.error = error; + } + + public CredsStoreHelperResult(Map data) { + this.data = data; + } + + public String getError() { + return error; + } + + public Map getData() { + return data; + } + + @Override + public boolean equals(Object o) { + if (this == o) {return true;} + if (o == null || getClass() != o.getClass()) {return false;} + CredsStoreHelperResult that = (CredsStoreHelperResult) o; + return Objects.equals(error, that.error) && Objects.equals(data, that.data); + } + + @Override + public int hashCode() { + return Objects.hash(error, data); + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/authentication/FileStore.java b/api-client/src/main/java/de/gesellix/docker/authentication/FileStore.java new file mode 100644 index 00000000..1717ed80 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/authentication/FileStore.java @@ -0,0 +1,53 @@ +package de.gesellix.docker.authentication; + +import java.util.Base64; +import java.util.Map; +import java.util.stream.Collectors; + +public class FileStore implements CredsStore { + + private final Map config; + private transient Map allAuthConfigs; + + public FileStore(Map config) { + this.config = config.containsKey("auths") ? (Map) config.get("auths") : config; + } + + @Override + public AuthConfig getAuthConfig(String registry) { + final AuthConfig authConfig = getAuthConfigs().get(registry); + return authConfig != null ? authConfig : AuthConfig.EMPTY_AUTH_CONFIG; + } + + @Override + public Map getAuthConfigs() { + if (allAuthConfigs == null) { + allAuthConfigs = config.entrySet().stream() + .filter((e) -> e.getValue() != null && (e.getValue().get("auth") != null || e.getValue().containsKey("identitytoken"))) + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> { + String registry = e.getKey(); + Map value = e.getValue(); + + AuthConfig authConfig = new AuthConfig(); + authConfig.setServeraddress(registry); + + if (value.containsKey("identitytoken")) { + authConfig.setIdentitytoken((String) value.get("identitytoken")); + } + else { + String[] login = new String(Base64.getDecoder().decode((String) value.get("auth"))).split(":"); + String username = login[0]; + String password = login[1]; + authConfig.setUsername(username); + authConfig.setPassword(password); + authConfig.setEmail((String) value.get("email")); + } + return authConfig; + } + )); + } + return allAuthConfigs; + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/authentication/NativeStore.java b/api-client/src/main/java/de/gesellix/docker/authentication/NativeStore.java new file mode 100644 index 00000000..6ed3addd --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/authentication/NativeStore.java @@ -0,0 +1,81 @@ +package de.gesellix.docker.authentication; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import static de.gesellix.docker.authentication.AuthConfig.EMPTY_AUTH_CONFIG; + +public class NativeStore implements CredsStore { + + private final static Logger log = LoggerFactory.getLogger(NativeStore.class); + + private final String credStoreName; + + CredsStoreHelper credsStoreHelper; + + public NativeStore(String credStoreName) { + this.credStoreName = credStoreName; + this.credsStoreHelper = new CredsStoreHelper(); + } + + @Override + public AuthConfig getAuthConfig(String registry) { + CredsStoreHelperResult creds = credsStoreHelper.getAuthentication(credStoreName, registry); + if (creds.getError() != null && !creds.getError().trim().isEmpty()) { + log.info("Error reading credentials from 'credsStore={}' for authentication at {}: {}", credStoreName, registry, creds.getError()); + return EMPTY_AUTH_CONFIG; + } + else if (creds.getData() != null && !creds.getData().isEmpty()) { + log.info("Got credentials from 'credsStore={}'", credStoreName); + AuthConfig result = parseCreds(creds.getData()); + result.setServeraddress(registry); + return result; + } + else { + log.warn("Using 'credsStore={}' for authentication at {} is currently not supported", credStoreName, registry); + return EMPTY_AUTH_CONFIG; + } + } + + @Override + public Map getAuthConfigs() { + final Map result = new HashMap<>(); + CredsStoreHelperResult creds = credsStoreHelper.getAllAuthentications(credStoreName); + if (creds.getError() != null && !creds.getError().trim().isEmpty()) { + log.info("Error reading credentials from 'credsStore={}': {}", credStoreName, creds.getError()); + return result; + } + else if (creds.getData() != null && !creds.getData().isEmpty()) { + log.info("Got credentials from 'credsStore={}'", credStoreName); + return creds.getData().keySet() + .stream() + .collect(Collectors.toMap( + k -> k, + this::getAuthConfig + )); + } + else { + log.warn("Using 'credsStore={}' is currently not supported", credStoreName); + return result; + } + } + + private AuthConfig parseCreds(Map creds) { + AuthConfig authDetails; + if (TOKEN_USERNAME.equals(creds.get("Username"))) { + authDetails = new AuthConfig(); + authDetails.setIdentitytoken((String) creds.get("Secret")); + } + else { + authDetails = new AuthConfig(); + authDetails.setUsername((String) creds.get("Username")); + authDetails.setPassword((String) creds.get("Secret")); + } + + return authDetails; + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/builder/BuildContextBuilder.java b/api-client/src/main/java/de/gesellix/docker/builder/BuildContextBuilder.java new file mode 100644 index 00000000..9d34d530 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/builder/BuildContextBuilder.java @@ -0,0 +1,85 @@ +package de.gesellix.docker.builder; + +import okio.BufferedSink; +import okio.BufferedSource; +import okio.Okio; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.zip.GZIPOutputStream; + +public class BuildContextBuilder { + + private static final Logger log = LoggerFactory.getLogger(BuildContextBuilder.class); + + public static void archiveTarFilesRecursively(File base, File targetFile) throws IOException { + List filenames = new DockerignoreFileFilter(base, new ArrayList<>(Collections.singletonList(targetFile.getAbsolutePath()))).collectFiles(base); + log.debug("found {} files in buildContext.", filenames.size()); + archiveTarFiles(base, filenames.stream().map(File::getAbsolutePath).collect(Collectors.toList()), targetFile); + } + + public static void archiveTarFilesRecursively(File base, OutputStream target) throws IOException { + List filenames = new DockerignoreFileFilter(base, new ArrayList<>()).collectFiles(base); + log.debug("found {} files in buildContext.", filenames.size()); + archiveTarFiles(base, filenames.stream().map(File::getAbsolutePath).collect(Collectors.toList()), target); + } + + public static void archiveTarFiles(File base, List filenames, File targetFile) throws IOException { + archiveTarFiles(base, filenames, Files.newOutputStream(targetFile.toPath())); + } + + public static void archiveTarFiles(File base, List filenames, OutputStream target) throws IOException { + try (TarArchiveOutputStream tos = new TarArchiveOutputStream(new GZIPOutputStream(target))) { + tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); + for (String filename : filenames) { + String relativeFileName = relativize(base, new File(filename)); + log.debug("adding {} as {}", filename, relativeFileName); + addAsTarEntry(new File(filename), relativeFileName, tos); + } + } + } + + public static void addAsTarEntry(File file, String relativeFileName, TarArchiveOutputStream tos) throws IOException { + TarArchiveEntry tarEntry = new TarArchiveEntry(file); + tarEntry.setName(relativeFileName); + + if (!file.isDirectory()) { + if (Files.isExecutable(file.toPath())) { + tarEntry.setMode(tarEntry.getMode() | 0755); + } + } + + tos.putArchiveEntry(tarEntry); + + if (!file.isDirectory()) { + copyFile(file, tos); + } + + tos.closeArchiveEntry(); + } + + public static String relativize(File base, File absolute) { + return base.toPath().relativize(absolute.toPath()).toString(); + } + + public static long copyFile(File input, OutputStream output) throws IOException { + BufferedSink sink = Okio.buffer(Okio.sink(output)); + try (BufferedSource source = Okio.buffer(Okio.source(input))) { + long read = source.readAll(sink); + sink.flush(); + return read; + } + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/builder/DockerignoreFileFilter.java b/api-client/src/main/java/de/gesellix/docker/builder/DockerignoreFileFilter.java new file mode 100644 index 00000000..c806c04a --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/builder/DockerignoreFileFilter.java @@ -0,0 +1,103 @@ +package de.gesellix.docker.builder; + +import okio.Okio; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class DockerignoreFileFilter { + + private static final Logger log = LoggerFactory.getLogger(DockerignoreFileFilter.class); + + private final GlobsMatcher globsMatcher; + + public DockerignoreFileFilter(File base) { + this(base, Collections.emptyList()); + } + + public DockerignoreFileFilter(File base, List additionalExcludes) { + List dockerignore = getDockerignorePatterns(base); + dockerignore.add(".dockerignore"); + dockerignore.addAll(additionalExcludes); + try { + dockerignore = relativize(dockerignore, base); + } + catch (IllegalArgumentException e) { + log.error(String.format("base: %1$s, dockerignore: %2$s", base.getAbsolutePath(), dockerignore), e); + throw e; + } + + log.debug("base: {}", base.getAbsolutePath()); + log.debug("dockerignore: {}", dockerignore); + globsMatcher = new GlobsMatcher(base, dockerignore); + } + + public List getDockerignorePatterns(final File base) { + List result = new ArrayList<>(); + File[] files = base.listFiles(); + if (files == null || files.length == 0) { + return result; + } + Optional dockerignoreFile = Arrays.stream(files).filter((file -> { + String relativeFileName = relativize(base, file); + return ".dockerignore".equals(relativeFileName); + })).findFirst(); + if (!dockerignoreFile.isPresent()) { + return result; + } + try { + Collections.addAll(result, Okio.buffer(Okio.source(Files.newInputStream(dockerignoreFile.get().toPath()))) + .readUtf8() + .split("[\r\n]+")); + return result; + } + catch (IOException e) { + log.error("Couldn't read {}", dockerignoreFile.get()); + throw new RuntimeException(e); + } + } + + public List relativize(Collection dockerignores, final File base) { + return dockerignores.stream() + .map((String dockerignore) -> new File(dockerignore).isAbsolute() ? relativize(base, new File(dockerignore)) : dockerignore) + .collect(Collectors.toList()); + } + + public String relativize(File base, File absolute) { + Path basePath = base.getAbsoluteFile().toPath(); + Path otherPath = absolute.getAbsoluteFile().toPath(); + if (!basePath.getRoot().equals(otherPath.getRoot())) { + // Can occur on Windows, when + // - java temp directory is under C:/ + // - project directory is under D:/ + return otherPath.toString(); + } + + return basePath.relativize(otherPath).toString(); + } + + public List collectFiles(File base) throws IOException { + final List files = new ArrayList<>(); + Files.walk(base.toPath()) + .filter(p -> Files.isRegularFile(p) && !getGlobsMatcher().matches(p.toFile())) + .forEach(p -> files.add(p.toFile())); + log.debug("filtered list of files: {}", files); + return files; + } + + public GlobsMatcher getGlobsMatcher() { + return globsMatcher; + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/builder/GlobsMatcher.java b/api-client/src/main/java/de/gesellix/docker/builder/GlobsMatcher.java new file mode 100644 index 00000000..3b4026de --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/builder/GlobsMatcher.java @@ -0,0 +1,117 @@ +package de.gesellix.docker.builder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class GlobsMatcher { + + private static final Logger log = LoggerFactory.getLogger(GlobsMatcher.class); + + private final File base; + private final List globs; + private List matchers; + + public GlobsMatcher(File base, List globs) { + this.base = base; + this.globs = globs; + } + + public void initMatchers() { + if (this.matchers == null) { + final FileSystem fileSystem = FileSystems.getDefault(); + this.matchers = new ArrayList<>(); + globs.stream() + .flatMap(glob -> { + if (glob.endsWith("/")) { + return Stream.of( + new Matcher(fileSystem, glob.replaceAll("/$", "")), + new Matcher(fileSystem, glob.replaceAll("/$", "/**"))); + } + else { + return Stream.of(new Matcher(fileSystem, glob)); + } + }) + .collect(Collectors.toCollection(ArrayDeque::new)) + .descendingIterator() // reverse the stream + .forEachRemaining(matchers::add); + if (log.isDebugEnabled()) { + matchers.forEach((m) -> log.debug("pattern: " + m.getPattern())); + } + } + } + + public boolean matches(File path) { + initMatchers(); + + final Path relativePath = base.getAbsoluteFile().toPath().relativize(path.getAbsoluteFile().toPath()); + Optional matcher = matchers.stream().filter((m) -> m.matches(relativePath)).findFirst(); + if (!matcher.isPresent() && relativePath.getParent() != null) { + matcher = matchers.stream().filter((m) -> m.matches(relativePath.getParent())).findFirst(); + } + + return matcher.isPresent() && !matcher.get().getNegate(); + } + + public List getMatchers() { + return matchers; + } + + public static class Matcher implements PathMatcher { + + private final String pattern; + private final PathMatcher matcher; + private final boolean negate; + + public Matcher(FileSystem fileSystem, String pattern) { + // According to https://docs.docker.com/engine/reference/builder/#dockerignore-file + // and https://golang.org/pkg/path/filepath/#Clean we clean paths + // by removing trailing slashes and also by replacing slashes with the path separator. + String replacement = File.separatorChar == '\\' ? "\\\\" : File.separatorChar + ""; + this.pattern = String.join(replacement, pattern.replaceAll("/", replacement).split(replacement)); + + String negation = "!"; + this.negate = pattern.startsWith(negation); + if (this.negate) { + String invertedPattern = this.pattern.substring(negation.length()); + this.matcher = createGlob(fileSystem, invertedPattern); + } + else { + this.matcher = createGlob(fileSystem, this.pattern); + } + } + + public static PathMatcher createGlob(FileSystem fileSystem, final String glob) { + return fileSystem.getPathMatcher("glob:" + glob); + } + + @Override + public boolean matches(Path path) { + return matcher.matches(path); + } + + public String getPattern() { + return pattern; + } + + public boolean getNegate() { + return negate; + } + + @Override + public String toString() { + return "matching " + (negate ? "!" : "") + pattern; + } + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/context/ContextStore.java b/api-client/src/main/java/de/gesellix/docker/context/ContextStore.java new file mode 100644 index 00000000..babc6d18 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/context/ContextStore.java @@ -0,0 +1,32 @@ +package de.gesellix.docker.context; + +import de.gesellix.docker.engine.DockerEnv; + +import java.io.File; +import java.util.Objects; + +public class ContextStore { + + private final MetadataStore metadataStore; + + public ContextStore(File dockerContextStoreDir) { + File metaRoot = new File(dockerContextStoreDir, MetadataStore.metadataDir); +// final String tlsDir = "tls"; +// File tlsRoot = new File(env.getDockerContextStoreDir(), tlsDir); + metadataStore = new MetadataStore(metaRoot); + } + + public Metadata getMetadata(String contextName) { + if (Objects.equals(contextName, DockerEnv.dockerDefaultContextName)) { + // should return the equivalent metadata of `docker context inspect default` + Metadata metadata = new Metadata(DockerEnv.dockerDefaultContextName); + metadata.setMetadata(new DockerContext("")); + metadata.getEndpoints().put( + DockerEnv.dockerEndpointDefaultName, + new EndpointMetaBase(DockerEnv.getDockerHostFromSystemPropertyOrEnvironment(), false)); + return metadata; + } + + return metadataStore.getMetadata(contextName); + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/context/DockerContext.java b/api-client/src/main/java/de/gesellix/docker/context/DockerContext.java new file mode 100644 index 00000000..8b271470 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/context/DockerContext.java @@ -0,0 +1,14 @@ +package de.gesellix.docker.context; + +import java.util.Map; + +public class DockerContext { + String description; + + // e.g. `"StackOrchestrator": "swarm"` + Map additionalFields; + + public DockerContext(String description) { + this.description = description; + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/context/DockerContextResolver.java b/api-client/src/main/java/de/gesellix/docker/context/DockerContextResolver.java new file mode 100644 index 00000000..a795ccc0 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/context/DockerContextResolver.java @@ -0,0 +1,48 @@ +package de.gesellix.docker.context; + +import de.gesellix.docker.engine.DockerConfigReader; +import de.gesellix.docker.engine.DockerEnv; + +import java.util.Map; + +public class DockerContextResolver { + + // see the original implementation at https://github.com/docker/cli/blob/de6020a240ff95c97150f07d7a0dd59981143868/cli/command/cli.go#L448 + public String resolveDockerContextName(DockerConfigReader dockerConfigReader) { + String dockerHost = DockerEnv.getDockerHostFromSystemPropertyOrEnvironment(); + String dockerContext = DockerEnv.getDockerContextFromSystemPropertyOrEnvironment(); +// if (dockerContext != null && dockerHost != null) { +// throw new IllegalStateException("Conflicting options: either specify --host or --context, not both"); +// } + if (dockerContext != null) { + return dockerContext; + } + if (dockerHost != null) { + return DockerEnv.dockerDefaultContextName; + } + Map configFile = dockerConfigReader.readDockerConfigFile(); + if (configFile != null && configFile.containsKey("currentContext")) { + // TODO ensure `currentContext` to be valid + // _, err := contextstore.GetMetadata(config.CurrentContext) + // if errdefs.IsNotFound(err) { + // return "", errors.Errorf("current context %q is not found on the file system, please check your config file at %s", config.CurrentContext, config.Filename) + // } + return (String) configFile.get("currentContext"); + } + return DockerEnv.dockerDefaultContextName; + } + + // see the original implementation at https://github.com/docker/cli/blob/de6020a240ff95c97150f07d7a0dd59981143868/cli/command/cli.go#L278 + public EndpointMetaBase resolveDockerEndpoint(ContextStore store, String contextName) { + Metadata metadata = store.getMetadata(contextName); + if (metadata == null || metadata.getEndpoints() == null || !metadata.getEndpoints().containsKey(DockerEnv.dockerEndpointDefaultName)) { + throw new IllegalStateException("cannot find docker endpoint in context " + contextName); + } + if (!(metadata.getEndpoints().get(DockerEnv.dockerEndpointDefaultName) instanceof EndpointMetaBase)) { + throw new IllegalStateException("endpoint " + DockerEnv.dockerEndpointDefaultName + " is not of type EndpointMetaBase"); +// throw new IllegalStateException("endpoint " + DockerEnv.dockerEndpointDefaultName + " is not of type EndpointMeta"); + } + // TODO TLSData + return (EndpointMetaBase) metadata.getEndpoints().get(DockerEnv.dockerEndpointDefaultName); + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/context/EndpointMetaBase.java b/api-client/src/main/java/de/gesellix/docker/context/EndpointMetaBase.java new file mode 100644 index 00000000..e1792424 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/context/EndpointMetaBase.java @@ -0,0 +1,34 @@ +package de.gesellix.docker.context; + +import java.util.Objects; + +public class EndpointMetaBase { + private String host; + private Boolean skipTLSVerify; + + public EndpointMetaBase(String host, Boolean skipTLSVerify) { + this.host = host; + this.skipTLSVerify = skipTLSVerify; + } + + public String getHost() { + return host; + } + + public Boolean getSkipTLSVerify() { + return skipTLSVerify; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EndpointMetaBase that = (EndpointMetaBase) o; + return Objects.equals(getHost(), that.getHost()) && Objects.equals(getSkipTLSVerify(), that.getSkipTLSVerify()); + } + + @Override + public int hashCode() { + return Objects.hash(getHost(), getSkipTLSVerify()); + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/context/Metadata.java b/api-client/src/main/java/de/gesellix/docker/context/Metadata.java new file mode 100644 index 00000000..5d9f0856 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/context/Metadata.java @@ -0,0 +1,32 @@ +package de.gesellix.docker.context; + +import java.util.HashMap; +import java.util.Map; + +public class Metadata { + private String name; + + private Object metadata; + + private Map endpoints = new HashMap<>(); + + public Metadata(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public Object getMetadata() { + return metadata; + } + + public void setMetadata(Object metadata) { + this.metadata = metadata; + } + + public Map getEndpoints() { + return endpoints; + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/context/MetadataStore.java b/api-client/src/main/java/de/gesellix/docker/context/MetadataStore.java new file mode 100644 index 00000000..65166183 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/context/MetadataStore.java @@ -0,0 +1,84 @@ +package de.gesellix.docker.context; + +import com.squareup.moshi.Moshi; +import de.gesellix.docker.engine.DockerEnv; +import okio.Okio; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileNotFoundException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.MessageFormat; +import java.util.Collections; +import java.util.Map; + +public class MetadataStore { + private final static Logger log = LoggerFactory.getLogger(MetadataStore.class); + + public final static String metadataDir = "meta"; + final String metaFile = "meta.json"; + + private final Moshi moshi = new Moshi.Builder().build(); + + File root; +// Config config; + + public MetadataStore(File root) { + this.root = root; + } + + public Metadata getMetadata(String contextName) { + return getByID(getContextDir(contextName)); + } + + public Metadata getByID(String contextDirectory) { + Map payload = getMetadataPayload(contextDirectory); + Metadata metadata = new Metadata((String) payload.get("Name")); + // TODO `metadata` should be read type safe + // see https://github.com/docker/cli/blob/09c94c1c21cb2ed02d347934de85b6163dc62ddf/cli/context/store/metadatastore.go#L83 + metadata.setMetadata(payload.get("Metadata")); + // TODO each `endpoint` should be read type safe + // see https://github.com/docker/cli/blob/09c94c1c21cb2ed02d347934de85b6163dc62ddf/cli/context/store/metadatastore.go#L87 + metadata.getEndpoints().putAll((Map) payload.get("Endpoints")); + if (metadata.getEndpoints().containsKey(DockerEnv.dockerEndpointDefaultName)) { + Map endpointMeta = (Map) metadata.getEndpoints().get(DockerEnv.dockerEndpointDefaultName); + metadata.getEndpoints().put( + DockerEnv.dockerEndpointDefaultName, + new EndpointMetaBase((String) endpointMeta.get("Host"), false)); + } + return metadata; + } + + // Code taken from https://stackoverflow.com/a/62401723/372019 + // SHA256 ist the current implementation in the docker/cli, + // see https://github.com/docker/cli/blob/cd7c493ea2cfb8c6db0beb65cf9830c8df23a9f9/cli/context/store/store.go#L8 + public String getContextDir(String contextName) { + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] hashBytes = messageDigest.digest(contextName.getBytes(StandardCharsets.UTF_8)); + BigInteger noHash = new BigInteger(1, hashBytes); + String hashStr = noHash.toString(16); + return hashStr; + } + + private Map getMetadataPayload(String contextDirectory) { + File contextMetadata = new File(new File(root, contextDirectory), metaFile); + if (!contextMetadata.exists()) { + throw new IllegalStateException("context does not exist", new FileNotFoundException(contextMetadata.getAbsolutePath())); + } + try { + return moshi.adapter(Map.class).fromJson(Okio.buffer(Okio.source(contextMetadata))); + } catch (Exception e) { + log.debug(MessageFormat.format("failed to read metadata from {}", contextMetadata), e); + return Collections.emptyMap(); + } + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/engine/DockerClientConfig.java b/api-client/src/main/java/de/gesellix/docker/engine/DockerClientConfig.java new file mode 100644 index 00000000..389c3044 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/engine/DockerClientConfig.java @@ -0,0 +1,225 @@ +package de.gesellix.docker.engine; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class DockerClientConfig { + + private final static Logger log = LoggerFactory.getLogger(DockerClientConfig.class); + + private DockerEnv env; + private String scheme; + private String host; + private int port; + private String certPath; + + public DockerClientConfig() { + this(new DockerEnv()); + } + + public DockerClientConfig(String dockerHost) { + this(new DockerEnv(dockerHost)); + } + + public DockerClientConfig(DockerEnv config) { + apply(config); + } + + public void apply(DockerEnv env) { + if (env.getDockerHost() == null || env.getDockerHost().isEmpty()) { + throw new IllegalStateException("dockerHost must be set"); + } + this.env = env; + + Map dockerClientConfig; + try { + dockerClientConfig = getActualConfig(env); + } + catch (MalformedURLException e) { + log.error("Invalid DOCKER_HOST " + env.getDockerHost(), e); + throw new RuntimeException("Invalid DOCKER_HOST " + env.getDockerHost(), e); + } + this.scheme = dockerClientConfig.get("protocol"); + this.host = dockerClientConfig.get("host"); + this.port = Integer.parseInt(dockerClientConfig.get("port")); + this.certPath = dockerClientConfig.get("certPath"); + } + + Map getActualConfig(DockerEnv env) throws MalformedURLException { + String dockerHost = env.getDockerHost(); + if (dockerHost == null || dockerHost.isEmpty()) { + throw new IllegalStateException("dockerHost must be set"); + } + final String oldProtocol = dockerHost.split("://", 2)[0]; + String protocol = oldProtocol; + final Map result = new HashMap<>(); + switch (protocol) { + case "http": + case "https": + case "tcp": + URL candidateURL = new URL(dockerHost.replaceFirst("^" + oldProtocol + "://", "https://")); + TlsConfig tlsConfig = getTlsConfig(candidateURL, env); + if (tlsConfig.getTlsVerify()) { + protocol = "https"; + result.put("certPath", tlsConfig.getCertPath()); + } + else { + protocol = "http"; + result.put("certPath", null); + } + + URL tcpUrl = new URL(dockerHost.replaceFirst("^" + oldProtocol + "://", protocol + "://")); + result.put("protocol", tcpUrl.getProtocol()); + result.put("host", tcpUrl.getHost()); + result.put("port", String.valueOf(tcpUrl.getPort())); + break; + case "unix": + String dockerUnixSocket = dockerHost.replaceFirst("unix://", ""); + result.put("protocol", "unix"); + result.put("host", dockerUnixSocket); + result.put("port", String.valueOf(-1)); + result.put("certPath", null); + break; + case "npipe": + String dockerNamedPipe = dockerHost.replaceFirst("npipe://", ""); + result.put("protocol", "npipe"); + result.put("host", dockerNamedPipe); + result.put("port", String.valueOf(-1)); + result.put("certPath", null); + break; + default: + log.warn("protocol '" + protocol + "' not supported"); + URL url = new URL(dockerHost); + result.put("protocol", url.getProtocol()); + result.put("host", url.getHost()); + result.put("port", String.valueOf(url.getPort())); + result.put("certPath", null); + break; + } + log.debug("selected dockerHost at '" + result + "'"); + return result; + } + + public TlsConfig getTlsConfig(URL candidateURL, final DockerEnv env) { + // Setting env.DOCKER_TLS_VERIFY to the empty string disables tls verification, + // while any other value (including "0" or "false") enables tls verification. + // See https://docs.docker.com/engine/reference/commandline/cli/#environment-variables + // for the official docs and https://github.com/moby/moby/issues/22411 for a detailed + // discussion about enabling/disabling TLS verification in Docker. + + // explicitly disabled? + if (env.getTlsVerify() != null && env.getTlsVerify().equals("")) { + log.debug("dockerTlsVerify='" + env.getTlsVerify() + "'"); + return new TlsConfig(false, null); + } + + String certPath = getCertPathOrNull(env); + final Boolean certsPathExists = certPath != null; + + // explicitly enabled? + if (env.getTlsVerify() != null && !env.getTlsVerify().isEmpty()) { + if (!certsPathExists) { + throw new IllegalStateException("tlsverify='" + env.getTlsVerify() + "', but '" + env.getCertPath() + "' doesn't exist"); + } + else { + log.debug("certsPathExists=" + certsPathExists); + return new TlsConfig(true, certPath); + } + } + + // make a guess if we could use tls, when it's neither explicitly enabled nor disabled + final Boolean isTlsPort = candidateURL.getPort() == env.getDefaultTlsPort(); + log.debug("certsPathExists=" + certsPathExists + ", isTlsPort=" + isTlsPort); + return new TlsConfig(certsPathExists && isTlsPort, certPath); + } + + public String getCertPathOrNull(final DockerEnv env) { + Boolean certsPathExists = env.getCertPath() != null && !env.getCertPath().isEmpty() && new File(env.getCertPath(), "").isDirectory(); + if (!certsPathExists) { + if (env.getDefaultCertPath() != null && !env.getDefaultCertPath().isEmpty() && new File(env.getDefaultCertPath(), "").isDirectory()) { + log.debug("defaultDockerCertPath=" + env.getDefaultCertPath()); + return env.getDefaultCertPath(); + } + return null; + } + else { + log.debug("dockerCertPath=" + env.getCertPath()); + return env.getCertPath(); + } + } + + public Boolean isContentTrustEnabled(DockerEnv env) { + if (env.getDockerContentTrust().trim().equals("") || isFalsy(env.getDockerContentTrust())) { + return false; + } + // is a truthy value or any other (non-empty and non-falsy) value + return true; + } + + public static Boolean isFalsy(String value) { + String sanitizedValue = value.trim().toLowerCase(); + return Arrays.asList("0", "false", "no").contains(sanitizedValue); + } + + public DockerEnv getEnv() { + return env; + } + + public void setEnv(DockerEnv env) { + this.env = env; + } + + public String getScheme() { + return scheme; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public String getCertPath() { + return certPath; + } + + public static class TlsConfig { + + private boolean tlsVerify = false; + private String certPath = null; + + public TlsConfig(boolean tlsVerify, String certPath) { + this.tlsVerify = tlsVerify; + this.certPath = certPath; + } + + public boolean getTlsVerify() { + return tlsVerify; + } + + public boolean isTlsVerify() { + return tlsVerify; + } + + public void setTlsVerify(boolean tlsVerify) { + this.tlsVerify = tlsVerify; + } + + public String getCertPath() { + return certPath; + } + + public void setCertPath(String certPath) { + this.certPath = certPath; + } + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/engine/DockerConfigReader.java b/api-client/src/main/java/de/gesellix/docker/engine/DockerConfigReader.java new file mode 100644 index 00000000..647d4187 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/engine/DockerConfigReader.java @@ -0,0 +1,81 @@ +package de.gesellix.docker.engine; + +import com.squareup.moshi.Moshi; + +import okio.Okio; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.text.MessageFormat; +import java.util.Collections; +import java.util.Map; + +public class DockerConfigReader { + + private static final Logger log = LoggerFactory.getLogger(DockerConfigReader.class); + + public File configFile = new File(System.getProperty("user.home") + "/.docker", "config.json"); + + public File legacyConfigFile = new File(System.getProperty("user.home"), ".dockercfg"); + + private File dockerConfigFile = null; + + private final Moshi moshi = new Moshi.Builder().build(); + + /** + * Visible internally and for tests + * + * @deprecated should ony be used in tests + */ + @Deprecated + public void resetDockerConfigFile() { + setDockerConfigFile(null); + } + + public void setDockerConfigFile(File dockerConfigFile) { + this.dockerConfigFile = dockerConfigFile; + } + + public File getDockerConfigFile() { + if (dockerConfigFile == null) { + dockerConfigFile = resolveDockerConfigFile(configFile, legacyConfigFile); + } + return dockerConfigFile; + } + + public File resolveDockerConfigFile(File defaultConfigFile, File legacyConfigFile) { + String dockerConfig = System.getProperty("docker.config", System.getenv("DOCKER_CONFIG")); + if (dockerConfig != null && !dockerConfig.isEmpty()) { + return new File(dockerConfig, "config.json"); + } else if (defaultConfigFile.exists()) { + return defaultConfigFile; + } else if (legacyConfigFile.exists()) { + return legacyConfigFile; + } + log.warn("docker config file not found, assuming '{}' as fallback", defaultConfigFile); + return defaultConfigFile; + } + + public Map readDockerConfigFile() { + return readDockerConfigFile(null); + } + + public Map readDockerConfigFile(File dockerCfg) { + if (dockerCfg == null) { + dockerCfg = getDockerConfigFile(); + } + if (dockerCfg == null || !dockerCfg.exists()) { + log.info("docker config '{}' doesn't exist", dockerCfg); + return Collections.emptyMap(); + } + log.debug("reading config from {}", dockerCfg); + try { + return moshi.adapter(Map.class).fromJson(Okio.buffer(Okio.source(dockerCfg))); + } catch (Exception e) { + log.debug(MessageFormat.format("failed to read config from {}", dockerCfg), e); + return Collections.emptyMap(); + } + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/engine/DockerEnv.java b/api-client/src/main/java/de/gesellix/docker/engine/DockerEnv.java new file mode 100644 index 00000000..61cf7e5e --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/engine/DockerEnv.java @@ -0,0 +1,225 @@ +package de.gesellix.docker.engine; + +import de.gesellix.docker.context.ContextStore; +import de.gesellix.docker.context.DockerContextResolver; +import de.gesellix.docker.context.EndpointMetaBase; + +import java.io.File; + +/** + * Configuration via environment variables should work like + * described in the official cli docs. + */ +public class DockerEnv { + + private String dockerHost; + + private int defaultTlsPort = 2376; + + private String tlsVerify = System.getProperty("docker.tls.verify", System.getenv("DOCKER_TLS_VERIFY")); + + private String certPath = System.getProperty("docker.cert.path", System.getenv("DOCKER_CERT_PATH")); + + private String defaultCertPath = new File((String) System.getProperties().get("user.home"), ".docker").getAbsolutePath(); + + // the v1 registry still seems to be valid for authentication. + private final String indexUrl_v1 = "https://index.docker.io/v1/"; + private final String indexUrl_v2 = "https://registry-1.docker.io"; + + private DockerConfigReader dockerConfigReader; + private DockerContextResolver dockerContextResolver; + + private String contextsDirectoryName = "contexts"; + + private File dockerContextStoreDir = null; + + public static final String dockerEndpointDefaultName = "docker"; + public static final String dockerDefaultContextName = "default"; + + private String apiVersion = System.getProperty("docker.api.version", System.getenv("DOCKER_API_VERSION")); + + private String tmpdir = System.getProperty("docker.tmpdir", System.getenv("DOCKER_TMPDIR")); + + private String dockerContentTrust = System.getProperty("docker.content.trust", System.getenv("DOCKER_CONTENT_TRUST")); + + private String contentTrustServer = System.getProperty("docker.content.trust.server", System.getenv("DOCKER_CONTENT_TRUST_SERVER")); + + private String officialNotaryServer = "https://notary.docker.io"; + + public DockerEnv() { + this(null); + } + + public DockerEnv(String dockerHost) { + // TODO allow configuration via "config file provider" for lazy config file resolution + this.dockerConfigReader = new DockerConfigReader(); + this.dockerContextResolver = new DockerContextResolver(); + this.resetDockerHostFromCurrentConfig(dockerHost); + } + + /** + * Visible internally and for tests + * + * @deprecated should ony be used in tests + */ + @Deprecated + void resetDockerHostFromCurrentConfig() { + this.resetDockerHostFromCurrentConfig(null); + } + + /** + * Visible internally and for tests + * + * @param dockerHostOverride optional override of any other configuration inputs + * @deprecated should ony be used in tests + */ + @Deprecated + void resetDockerHostFromCurrentConfig(String dockerHostOverride) { + this.dockerContextStoreDir = null; + getDockerConfigReader().resetDockerConfigFile(); + if (dockerHostOverride == null) { + this.dockerHost = getDockerHostFromContextOrHostOrDefault(); + } else { + this.dockerHost = dockerHostOverride; + } + } + + private String getDockerHostFromContextOrHostOrDefault() { + // TODO allow configuration via "contexts directory provider" for lazy contexts directory resolution + ContextStore store = new ContextStore(getDockerContextStoreDir()); + String dockerContextName = dockerContextResolver.resolveDockerContextName(getDockerConfigReader()); + EndpointMetaBase dockerEndpoint = dockerContextResolver.resolveDockerEndpoint(store, dockerContextName); + if (dockerEndpoint != null && dockerEndpoint.getHost() != null) { + return dockerEndpoint.getHost(); + } else { + return getDefaultDockerHost(); + } + } + + public static String getDockerHostFromSystemPropertyOrEnvironment() { + String configuredDockerHost = System.getProperty("docker.host", System.getenv("DOCKER_HOST")); + if (configuredDockerHost != null && !configuredDockerHost.isEmpty()) { + return configuredDockerHost; + } + return null; + } + + public static String getDockerContextFromSystemPropertyOrEnvironment() { + String configuredDockerContext = System.getProperty("docker.context", System.getenv("DOCKER_CONTEXT")); + if (configuredDockerContext != null && !configuredDockerContext.isEmpty()) { + return configuredDockerContext; + } + return null; + } + + public static String getDefaultDockerHost() { + if (((String) System.getProperties().get("os.name")).toLowerCase().contains("windows")) { + return "npipe:////./pipe/docker_engine"; + } else { + return "unix:///var/run/docker.sock"; + } + } + + public File getDockerConfigFile() { + return dockerConfigReader.getDockerConfigFile(); + } + + public DockerConfigReader getDockerConfigReader() { + return dockerConfigReader; + } + + public File getDockerContextStoreDir() { + if (dockerContextStoreDir == null) { + dockerContextStoreDir = new File(getDockerConfigFile().getParentFile(), contextsDirectoryName); + } + return dockerContextStoreDir; + } + + public String getDockerHost() { + return dockerHost; + } + + public void setDockerHost(String dockerHost) { + this.dockerHost = dockerHost; + } + + public int getDefaultTlsPort() { + return defaultTlsPort; + } + + public void setDefaultTlsPort(int defaultTlsPort) { + this.defaultTlsPort = defaultTlsPort; + } + + public String getTlsVerify() { + return tlsVerify; + } + + public void setTlsVerify(String tlsVerify) { + this.tlsVerify = tlsVerify; + } + + public String getCertPath() { + return certPath; + } + + public void setCertPath(String certPath) { + this.certPath = certPath; + } + + public String getDefaultCertPath() { + return defaultCertPath; + } + + public void setDefaultCertPath(String defaultCertPath) { + this.defaultCertPath = defaultCertPath; + } + + public String getIndexUrl_v1() { + return indexUrl_v1; + } + + public String getIndexUrl_v2() { + return indexUrl_v2; + } + + public String getApiVersion() { + return apiVersion; + } + + public void setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + } + + public String getTmpdir() { + return tmpdir; + } + + public void setTmpdir(String tmpdir) { + this.tmpdir = tmpdir; + } + + public String getDockerContentTrust() { + return dockerContentTrust; + } + + public void setDockerContentTrust(String dockerContentTrust) { + this.dockerContentTrust = dockerContentTrust; + } + + public String getContentTrustServer() { + return contentTrustServer; + } + + public void setContentTrustServer(String contentTrustServer) { + this.contentTrustServer = contentTrustServer; + } + + public String getOfficialNotaryServer() { + return officialNotaryServer; + } + + public void setOfficialNotaryServer(String officialNotaryServer) { + this.officialNotaryServer = officialNotaryServer; + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/engine/DockerVersion.java b/api-client/src/main/java/de/gesellix/docker/engine/DockerVersion.java new file mode 100644 index 00000000..65d782b7 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/engine/DockerVersion.java @@ -0,0 +1,86 @@ +package de.gesellix.docker.engine; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +public class DockerVersion implements Comparable { + + private int major; + private int minor; + private int patch; + private String meta; + + public static DockerVersion parseDockerVersion(String version) { + final Pattern versionPattern = Pattern.compile("(\\d+)\\.(\\d+)(?:\\.(\\d+)(.*))?"); + + final DockerVersion parsedVersion = new DockerVersion(); + Matcher matcher = versionPattern.matcher(version); + if (!matcher.matches()) { + throw new IllegalArgumentException(String.format("Version does not match the expected version pattern: '%s'", version)); + } + parsedVersion.setMajor(Integer.parseInt(matcher.group(1))); + parsedVersion.setMinor(Integer.parseInt(matcher.group(2))); + final String s = matcher.group(3); + parsedVersion.setPatch(Integer.parseInt(s != null && !s.isEmpty() ? s : "0")); + final String s1 = matcher.group(4); + parsedVersion.setMeta(s1 != null && !s1.isEmpty() ? s1 : ""); + return parsedVersion; + } + + @Override + public String toString() { + return getMajor() + "." + getMinor() + "." + getPatch() + getMeta(); + } + + @Override + public int compareTo(DockerVersion other) { + final ArrayList self = new ArrayList<>(Arrays.asList(this.major, this.minor, this.patch)); + final ArrayList that = new ArrayList<>(Arrays.asList(other.getMajor(), other.getMinor(), other.getPatch())); + + AtomicInteger result = new AtomicInteger(0); + IntStream.range(0, 2).forEach((index) -> { + int compared = self.get(index).compareTo(that.get(index)); + if (compared != 0 && result.get() == 0) { + result.set(compared); + } + }); + + return result.get(); + } + + public int getMajor() { + return major; + } + + public void setMajor(int major) { + this.major = major; + } + + public int getMinor() { + return minor; + } + + public void setMinor(int minor) { + this.minor = minor; + } + + public int getPatch() { + return patch; + } + + public void setPatch(int patch) { + this.patch = patch; + } + + public String getMeta() { + return meta; + } + + public void setMeta(String meta) { + this.meta = meta; + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/engine/RequestMethod.java b/api-client/src/main/java/de/gesellix/docker/engine/RequestMethod.java new file mode 100644 index 00000000..36e020f7 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/engine/RequestMethod.java @@ -0,0 +1,12 @@ +package de.gesellix.docker.engine; + +public enum RequestMethod { + + GET, + DELETE, + HEAD, + OPTIONS, + PATCH, + POST, + PUT +} diff --git a/api-client/src/main/java/de/gesellix/docker/json/CustomObjectAdapterFactory.java b/api-client/src/main/java/de/gesellix/docker/json/CustomObjectAdapterFactory.java new file mode 100644 index 00000000..1ea29f4a --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/json/CustomObjectAdapterFactory.java @@ -0,0 +1,22 @@ +package de.gesellix.docker.json; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Set; + +public class CustomObjectAdapterFactory implements JsonAdapter.Factory { + + @Override + public JsonAdapter create(Type type, Set annotations, Moshi moshi) { + + if (!type.equals(Object.class)) { + return null; + } + + JsonAdapter delegate = moshi.nextAdapter(this, Object.class, annotations); + return new NumberToBigDecimalJsonAdapter(delegate); + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/json/NumberToBigDecimalJsonAdapter.java b/api-client/src/main/java/de/gesellix/docker/json/NumberToBigDecimalJsonAdapter.java new file mode 100644 index 00000000..6e6b7592 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/json/NumberToBigDecimalJsonAdapter.java @@ -0,0 +1,38 @@ +package de.gesellix.docker.json; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; + +import java.io.IOException; +import java.math.BigDecimal; + +public class NumberToBigDecimalJsonAdapter extends JsonAdapter { + + private final JsonAdapter delegate; + + public NumberToBigDecimalJsonAdapter(JsonAdapter delegate) { + this.delegate = delegate; + } + + @Override + public Object fromJson(JsonReader reader) throws IOException { + if (reader.peek().equals(JsonReader.Token.NUMBER)) { + // allows Integer or Long values instead of strictly using Double as value type. + return new BigDecimal(reader.nextString()); + } + else { + return delegate.fromJson(reader); + } + } + + @Override + public void toJson(JsonWriter writer, Object value) throws IOException { + if (value instanceof Number) { + writer.jsonValue(value); + } + else { + delegate.toJson(writer, value); + } + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/response/JsonChunksReader.java b/api-client/src/main/java/de/gesellix/docker/response/JsonChunksReader.java new file mode 100644 index 00000000..8c15f03f --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/response/JsonChunksReader.java @@ -0,0 +1,39 @@ +package de.gesellix.docker.response; + +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.Moshi; +import de.gesellix.docker.json.CustomObjectAdapterFactory; +import okio.Okio; +import okio.Source; + +import java.io.IOException; + +public class JsonChunksReader implements Reader { + + private final JsonReader reader; + private final Moshi moshi; + + public JsonChunksReader(Source source) { + this(source, new Moshi.Builder().add(new CustomObjectAdapterFactory()).build()); + } + + public JsonChunksReader(Source source, Moshi moshi) { + this.moshi = moshi; + this.reader = JsonReader.of(Okio.buffer(source)); + // For transfer-encoding: chunked: + // allows repeated `readNext` calls to consume + // a complete stream of JSON chunks (delimited or not). + this.reader.setLenient(true); + } + + @Override + public T readNext(Class type) throws IOException { + return moshi.adapter(type).fromJson(reader); +// return reader.readJsonValue(); + } + + @Override + public boolean hasNext() throws IOException { + return !Thread.currentThread().isInterrupted() && reader.hasNext(); + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/response/Reader.java b/api-client/src/main/java/de/gesellix/docker/response/Reader.java new file mode 100644 index 00000000..6baa7f15 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/response/Reader.java @@ -0,0 +1,10 @@ +package de.gesellix.docker.response; + +import java.io.IOException; + +public interface Reader { + + T readNext(Class type) throws IOException; + + boolean hasNext() throws IOException; +} diff --git a/api-client/src/main/java/de/gesellix/docker/ssl/DockerSslSocket.java b/api-client/src/main/java/de/gesellix/docker/ssl/DockerSslSocket.java new file mode 100644 index 00000000..e202a34a --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/ssl/DockerSslSocket.java @@ -0,0 +1,26 @@ +package de.gesellix.docker.ssl; + +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; + +public class DockerSslSocket { + + public SSLSocketFactory getSslSocketFactory() { + return sslSocketFactory; + } + + public void setSslSocketFactory(SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + } + + public X509TrustManager getTrustManager() { + return trustManager; + } + + public void setTrustManager(X509TrustManager trustManager) { + this.trustManager = trustManager; + } + + private SSLSocketFactory sslSocketFactory; + private X509TrustManager trustManager; +} diff --git a/api-client/src/main/java/de/gesellix/docker/ssl/KeyStoreUtil.java b/api-client/src/main/java/de/gesellix/docker/ssl/KeyStoreUtil.java new file mode 100644 index 00000000..996da055 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/ssl/KeyStoreUtil.java @@ -0,0 +1,103 @@ +package de.gesellix.docker.ssl; + +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.Collection; +import java.util.Collections; + +/** + * A slightly modified copy from https://github.com/rhuss/docker-maven-plugin + * with kind permission of Roland Huss (https://twitter.com/ro14nd). + */ +public class KeyStoreUtil { + + private static final Logger log = LoggerFactory.getLogger(KeyStoreUtil.class); + + static { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + public static final char[] KEY_STORE_PASSWORD = "docker".toCharArray(); + + public static KeyStore createDockerKeyStore(String certPath) throws IOException, GeneralSecurityException { + PrivateKey privKey = loadPrivateKey(new File(certPath, "key.pem").getAbsolutePath()); + Certificate[] certs = loadCertificates(new File(certPath, "cert.pem").getAbsolutePath()).toArray(new Certificate[] {}); + + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + + keyStore.setKeyEntry("docker", privKey, KEY_STORE_PASSWORD, certs); + addCA(keyStore, new File(certPath, "ca.pem").getAbsolutePath()); + return keyStore; + } + + public static PrivateKey loadPrivateKey(String keyPath) throws IOException, GeneralSecurityException { + try (PEMParser parser = new PEMParser(new FileReader(keyPath))) { + Object parsedObject; + while ((parsedObject = parser.readObject()) != null) { + if (parsedObject instanceof PEMKeyPair) { + PEMKeyPair keyPair = (PEMKeyPair) parsedObject; + return generatePrivateKey(keyPair.getPrivateKeyInfo()); + } + else if (parsedObject instanceof PrivateKeyInfo) { + return generatePrivateKey((PrivateKeyInfo) parsedObject); + } + } + } + throw new GeneralSecurityException("Cannot generate private key from file: " + keyPath); + } + + public static PrivateKey generatePrivateKey(final PrivateKeyInfo keyInfo) throws IOException { + try { + return new JcaPEMKeyConverter().getPrivateKey(keyInfo); + } + catch (Exception e) { + if (e.getCause() instanceof InvalidKeySpecException) { + log.error("couldn't create private key for asn1oid '" + keyInfo.getPrivateKeyAlgorithm().getAlgorithm().getId() + "'", e.getCause()); + } + throw e; + } + } + + public static void addCA(final KeyStore keyStore, String caPath) throws KeyStoreException, CertificateException { + for (Certificate cert : loadCertificates(caPath)) { + X509Certificate crt = (X509Certificate) cert; + String alias = crt.getSubjectX500Principal().getName(); + keyStore.setCertificateEntry(alias, crt); + } + } + + public static Collection loadCertificates(String certPath) throws CertificateException { + try (InputStream is = Files.newInputStream(Paths.get(certPath))) { + return CertificateFactory.getInstance("X509").generateCertificates(is); + } + catch (IOException ignored) { + // silently ignored + return Collections.emptyList(); + } + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/ssl/SslSocketConfigFactory.java b/api-client/src/main/java/de/gesellix/docker/ssl/SslSocketConfigFactory.java new file mode 100644 index 00000000..931fbaed --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/ssl/SslSocketConfigFactory.java @@ -0,0 +1,75 @@ +package de.gesellix.docker.ssl; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.File; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; + +public class SslSocketConfigFactory { + + private static final Logger log = LoggerFactory.getLogger(SslSocketConfigFactory.class); + + public DockerSslSocket createDockerSslSocket(String certPath) { + SSLContext sslContext; + X509TrustManager trustManager; + try { + KeyStore keyStore = createKeyStore(certPath); + KeyManagerFactory keyManagerFactory = initKeyManagerFactory(keyStore); + TrustManagerFactory tmf = initTrustManagerFactory(keyStore); + trustManager = getUniqueX509TrustManager(tmf); + sslContext = initSslContext(keyManagerFactory, trustManager); + } + catch (Exception e) { + log.error("SSL initialization failed", e); + throw new RuntimeException("SSL initialization failed", e); + } + + DockerSslSocket socket = new DockerSslSocket(); + socket.setSslSocketFactory(sslContext.getSocketFactory()); + socket.setTrustManager(trustManager); + return socket; + } + + private SSLContext initSslContext(KeyManagerFactory keyManagerFactory, X509TrustManager trustManager) throws NoSuchAlgorithmException, KeyManagementException { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), new TrustManager[] {trustManager}, null); + return sslContext; + } + + private X509TrustManager getUniqueX509TrustManager(TrustManagerFactory trustManagerFactory) { + final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers"); + } + + return (X509TrustManager) trustManagers[0]; + } + + private TrustManagerFactory initTrustManagerFactory(KeyStore keyStore) throws NoSuchAlgorithmException, KeyStoreException { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + return trustManagerFactory; + } + + private KeyManagerFactory initKeyManagerFactory(KeyStore keyStore) throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException { + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, KeyStoreUtil.KEY_STORE_PASSWORD); + return keyManagerFactory; + } + + private KeyStore createKeyStore(String dockerCertPath) throws GeneralSecurityException, IOException { + return KeyStoreUtil.createDockerKeyStore(new File(dockerCertPath).getAbsolutePath()); + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/websocket/DefaultWebSocketListener.java b/api-client/src/main/java/de/gesellix/docker/websocket/DefaultWebSocketListener.java new file mode 100644 index 00000000..b8aa7c30 --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/websocket/DefaultWebSocketListener.java @@ -0,0 +1,45 @@ +package de.gesellix.docker.websocket; + +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DefaultWebSocketListener extends WebSocketListener { + + private final static Logger log = LoggerFactory.getLogger(DefaultWebSocketListener.class); + + @Override + public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { + log.debug("[onOpen]"); + } + + @Override + public void onFailure(@NotNull WebSocket webSocket, final Throwable t, Response response) { + log.debug("[onFailure] {}", t.getMessage()); + t.printStackTrace(); + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull final String text) { + log.debug("[onMessage.text] {}", text); + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, final ByteString bytes) { + log.debug("[onMessage.binary] size: {}", bytes.size()); + } + + @Override + public void onClosing(@NotNull WebSocket webSocket, final int code, @NotNull final String reason) { + log.debug("[onClosing] {}/{}", code, reason); + } + + @Override + public void onClosed(@NotNull WebSocket webSocket, final int code, @NotNull final String reason) { + log.debug("[onClosed] {}/{}", code, reason); + } +} diff --git a/api-client/src/main/java/de/gesellix/docker/websocket/WebsocketStatusCode.java b/api-client/src/main/java/de/gesellix/docker/websocket/WebsocketStatusCode.java new file mode 100644 index 00000000..95dccfad --- /dev/null +++ b/api-client/src/main/java/de/gesellix/docker/websocket/WebsocketStatusCode.java @@ -0,0 +1,26 @@ +package de.gesellix.docker.websocket; + +public enum WebsocketStatusCode { + NORMAL_CLOSURE(1000), + GOING_AWAY(1001), + PROTOCOL_ERROR(1002), + UNSUPPORTED_DATA(1003), + NO_STATUS_RCVD(1005), + ABNORMAL_CLOSURE(1006), + INVALID_FRAME_PAYLOAD_DATA(1007), + POLICY_VIOLATION(1008), + MESSAGE_TOO_BIG(1009), + MANDATORY_EXT(1010), + INTERNAL_SERVER_ERROR(1011), + TLS_HANDSHAKE(1015); + + WebsocketStatusCode(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + private final int code; +} diff --git a/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/ApiClient.kt b/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/ApiClient.kt index ab535160..13802938 100644 --- a/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/ApiClient.kt +++ b/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/ApiClient.kt @@ -6,7 +6,6 @@ import de.gesellix.docker.client.filesocket.UnixSocket import de.gesellix.docker.client.filesocket.UnixSocketFactory import de.gesellix.docker.client.filesocket.UnixSocketFactorySupport import de.gesellix.docker.engine.DockerClientConfig -import de.gesellix.docker.engine.EngineRequest import de.gesellix.docker.engine.RequestMethod.DELETE import de.gesellix.docker.engine.RequestMethod.GET import de.gesellix.docker.engine.RequestMethod.HEAD @@ -32,6 +31,8 @@ import java.lang.reflect.Type import java.net.Proxy import java.util.* import java.util.concurrent.TimeUnit +import kotlin.time.Duration +import kotlin.time.DurationUnit open class ApiClient( private val dockerClientConfig: DockerClientConfig = DockerClientConfig(), @@ -63,11 +64,6 @@ open class ApiClient( val socketFactories: MutableMap OkHttpClient.Builder> = mutableMapOf() -// @JvmStatic -// val engineClient: EngineClient by lazy { -// OkDockerClient() -// } - @JvmStatic val client: OkHttpClient by lazy { builder.build() @@ -125,50 +121,30 @@ open class ApiClient( } protected inline fun request(requestConfig: RequestConfig): ApiInfrastructureResponse { - val engineRequest = EngineRequest(requestConfig.method, requestConfig.path).also { - it.headers = requestConfig.headers - it.query = requestConfig.query - it.body = requestConfig.body - } - val request = prepareRequest(engineRequest) - val client = prepareClient(engineRequest) + val request = prepareRequest(requestConfig) + val client = prepareClient(requestConfig.timeout) return request(request, client, requestConfig.elementType) } protected fun requestWebSocket(requestConfig: RequestConfig, wsListener: WebSocketListener): WebSocket { - val engineRequest = EngineRequest(requestConfig.method, requestConfig.path).also { - it.headers = requestConfig.headers - it.query = requestConfig.query - it.body = requestConfig.body - } - val request = prepareRequest(engineRequest, JsonMediaType) - val client = prepareClient(engineRequest) + val request = prepareRequest(requestConfig, JsonMediaType) + val client = prepareClient(requestConfig.timeout) return client.newWebSocket(request, wsListener) } protected inline fun requestStream(requestConfig: RequestConfig): ApiInfrastructureResponse { - val engineRequest = EngineRequest(requestConfig.method, requestConfig.path).also { - it.headers = requestConfig.headers - it.query = requestConfig.query - it.body = requestConfig.body - } - val request = prepareRequest(engineRequest, JsonMediaType) - val client = prepareClient(engineRequest) + val request = prepareRequest(requestConfig, JsonMediaType) + val client = prepareClient(requestConfig.timeout) return requestStream(request, client) } protected fun requestFrames(requestConfig: RequestConfig): ApiInfrastructureResponse { - val engineRequest = EngineRequest(requestConfig.method, requestConfig.path).also { - it.headers = requestConfig.headers - it.query = requestConfig.query - it.body = requestConfig.body - } - val request = prepareRequest(engineRequest) - val client = prepareClient(engineRequest) + val request = prepareRequest(requestConfig) + val client = prepareClient(requestConfig.timeout) return requestFrames(request, client) } - protected fun prepareRequest(requestConfig: EngineRequest, fallbackContentType: String = ""): Request { + protected fun prepareRequest(requestConfig: RequestConfig, fallbackContentType: String = ""): Request { val httpUrl = buildHttpUrl().build() val pathWithOptionalApiVersion = when { @@ -241,14 +217,14 @@ open class ApiClient( return requestBuilder.build() } - protected fun prepareClient(requestConfig: EngineRequest): OkHttpClient { + protected fun prepareClient(timeout: Duration): OkHttpClient { // Logger.getLogger(OkHttpClient::class.java.name).level = Level.FINE // val engineResponse = engineClient.request(requestConfig) val actualClient = buildHttpClient(client.newBuilder()) // .proxy(proxy) // TODO // do we need to disable the timeout for streaming? - .connectTimeout(requestConfig.timeout.toLong(), TimeUnit.MILLISECONDS) - .readTimeout(requestConfig.timeout.toLong(), TimeUnit.MILLISECONDS) + .connectTimeout(timeout.toLong(DurationUnit.MILLISECONDS), TimeUnit.MILLISECONDS) + .readTimeout(timeout.toLong(DurationUnit.MILLISECONDS), TimeUnit.MILLISECONDS) .addInterceptor(EnforceResponseContentTypeInterceptor()) return actualClient.build() } diff --git a/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/FrameReader.kt b/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/FrameReader.kt index f3dd61be..9939478a 100644 --- a/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/FrameReader.kt +++ b/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/FrameReader.kt @@ -15,7 +15,7 @@ class FrameReader(source: Source, private val mediaType: String) : Reader override fun readNext(type: Class?): Frame { // see https://docs.docker.com/reference/api/engine/version-history/#v142-api-changes // see https://github.com/moby/moby/pull/39812 - return if (mediaType == ApiClient.Companion.DockerMultiplexedStreamMediaType) { + return if (mediaType == ApiClient.DockerMultiplexedStreamMediaType) { // See https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach for the stream format documentation. // header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} @@ -40,7 +40,7 @@ class FrameReader(source: Source, private val mediaType: String) : Reader !Thread.currentThread().isInterrupted // && bufferedSource.isOpen && !bufferedSource.peek().exhausted() - } catch (e: Exception) { + } catch (_: Exception) { return false } } diff --git a/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/RequestConfig.kt b/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/RequestConfig.kt index b55dfbf8..0281a079 100644 --- a/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/RequestConfig.kt +++ b/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/RequestConfig.kt @@ -2,6 +2,7 @@ package de.gesellix.docker.remote.api.core import de.gesellix.docker.engine.RequestMethod import java.lang.reflect.Type +import kotlin.time.Duration /** * Defines a config object for a given request. @@ -17,5 +18,7 @@ data class RequestConfig( val headers: MutableMap = mutableMapOf(), val query: MutableMap> = mutableMapOf(), val body: Any? = null, - val elementType: Type? = null + val elementType: Type? = null, + val apiVersion: String? = null, + val timeout: Duration = Duration.ZERO ) diff --git a/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/ResponseConsumer.kt b/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/ResponseConsumer.kt index 6be03205..bc1bd6bb 100644 --- a/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/ResponseConsumer.kt +++ b/api-client/src/main/kotlin/de/gesellix/docker/remote/api/core/ResponseConsumer.kt @@ -81,8 +81,8 @@ fun Socket?.consumeFrames(mediaType: String?): Flow { return emptyFlow() } when (mediaType) { - ApiClient.Companion.DockerMultiplexedStreamMediaType, - ApiClient.Companion.DockerRawStreamMediaType -> { + ApiClient.DockerMultiplexedStreamMediaType, + ApiClient.DockerRawStreamMediaType -> { val reader = FrameReader(source, mediaType) val events = flow { while (reader.hasNext()) { @@ -105,8 +105,8 @@ fun ResponseBody?.consumeFrames(mediaType: String?): Flow { return emptyFlow() } when (mediaType) { - ApiClient.Companion.DockerMultiplexedStreamMediaType, - ApiClient.Companion.DockerRawStreamMediaType -> { + ApiClient.DockerMultiplexedStreamMediaType, + ApiClient.DockerRawStreamMediaType -> { val reader = FrameReader(source(), mediaType) val events = flow { while (reader.hasNext()) { diff --git a/api-client/src/test/groovy/de/gesellix/docker/authentication/AuthConfigReaderTest.groovy b/api-client/src/test/groovy/de/gesellix/docker/authentication/AuthConfigReaderTest.groovy new file mode 100644 index 00000000..6dc68380 --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/authentication/AuthConfigReaderTest.groovy @@ -0,0 +1,178 @@ +package de.gesellix.docker.authentication + +import de.gesellix.docker.engine.DockerConfigReader +import de.gesellix.docker.engine.DockerEnv +import de.gesellix.testutil.ResourceReader +import spock.lang.Requires +import spock.lang.Specification + +import static de.gesellix.docker.authentication.AuthConfig.EMPTY_AUTH_CONFIG + +class AuthConfigReaderTest extends Specification { + + DockerEnv env + DockerConfigReader dockerConfigReader + AuthConfigReader authConfigReader + + def setup() { + dockerConfigReader = Spy(DockerConfigReader) + env = Mock(DockerEnv) { it.getDockerConfigReader() >> dockerConfigReader } + authConfigReader = Spy(AuthConfigReader, constructorArgs: [env]) + } + + def "read authConfig (new format)"() { + given: + String oldDockerConfig = System.clearProperty("docker.config") + File expectedConfigFile = new ResourceReader().getClasspathResourceAsFile('/auth/config.json', AuthConfigReader) + env.indexUrl_v1 >> 'https://index.docker.io/v1/' + + when: + AuthConfig result = authConfigReader.readAuthConfig(null, expectedConfigFile) + + then: + result == new AuthConfig(username: "gesellix", + password: "-yet-another-password-", + email: "tobias@gesellix.de", + serveraddress: "https://index.docker.io/v1/") + + cleanup: + if (oldDockerConfig) { + System.setProperty("docker.config", oldDockerConfig) + } else { + System.clearProperty("docker.config") + } + } + + def "read authConfig (legacy format)"() { + given: + String oldDockerConfig = System.clearProperty("docker.config") + File expectedConfigFile = new ResourceReader().getClasspathResourceAsFile('/auth/dockercfg', AuthConfigReader) + env.indexUrl_v1 >> 'https://index.docker.io/v1/' + + when: + AuthConfig result = authConfigReader.readAuthConfig(null, expectedConfigFile) + + then: + result == new AuthConfig(username: "gesellix", + password: "-yet-another-password-", + email: "tobias@gesellix.de", + serveraddress: "https://index.docker.io/v1/") + + cleanup: + if (oldDockerConfig) { + System.setProperty("docker.config", oldDockerConfig) + } else { + System.clearProperty("docker.config") + } + } + + def "read auth config for official Docker index"() { + given: + env.indexUrl_v1 >> 'https://index.docker.io/v1/' + File dockerCfg = new ResourceReader().getClasspathResourceAsFile('/auth/config.json', AuthConfigReader) + + when: + AuthConfig authDetails = authConfigReader.readAuthConfig(null, dockerCfg) + + then: + authDetails.username == "gesellix" + and: + authDetails.password == "-yet-another-password-" + and: + authDetails.email == "tobias@gesellix.de" + and: + authDetails.serveraddress == "https://index.docker.io/v1/" + } + + def "read auth config for quay.io"() { + given: + File dockerCfg = new ResourceReader().getClasspathResourceAsFile('/auth/config.json', AuthConfigReader) + + when: + AuthConfig authDetails = authConfigReader.readAuthConfig("quay.io", dockerCfg) + + then: + authDetails.username == "gesellix" + and: + authDetails.password == "-a-password-for-quay-" + and: + authDetails.email == "tobias@gesellix.de" + and: + authDetails.serveraddress == "quay.io" + } + + def "read auth config for missing config file"() { + given: + File nonExistingFile = new File('./I should not exist') + assert !nonExistingFile.exists() + + when: + AuthConfig authDetails = authConfigReader.readAuthConfig(null, nonExistingFile) + + then: + authDetails == new AuthConfig() + } + + def "read auth config for unknown registry hostname"() { + given: + File dockerCfg = new ResourceReader().getClasspathResourceAsFile('/auth/config.json', AuthConfigReader) + + when: + AuthConfig authDetails = authConfigReader.readAuthConfig("unknown.example.com", dockerCfg) + + then: + authDetails == EMPTY_AUTH_CONFIG + } + + @Requires({ System.properties['user.name'] == 'gesellix' }) + def "read default docker config file using credsStore"() { + given: + String oldDockerConfig = System.clearProperty("docker.config") + String configFile = "/auth/dockercfg-with-credsStore-${System.properties['os.name'].toString().toLowerCase().capitalize().replaceAll("\\s", "_")}" + File expectedConfigFile = new ResourceReader().getClasspathResourceAsFile(configFile, AuthConfigReader) + env.indexUrl_v1 >> 'https://index.docker.io/v1/' + dockerConfigReader.getDockerConfigFile() >> expectedConfigFile + + when: + AuthConfig authConfig = authConfigReader.readDefaultAuthConfig() + + then: + 1 * authConfigReader.readAuthConfig(null, expectedConfigFile) + authConfig.serveraddress == "https://index.docker.io/v1/" + authConfig.username == "gesellix" + authConfig.password =~ ".+" + + cleanup: + if (oldDockerConfig) { + System.setProperty("docker.config", oldDockerConfig) + } else { + System.clearProperty("docker.config") + } + } + + def "read default authConfig"() { + given: + String oldDockerConfig = System.clearProperty("docker.config") + String configFile = "/auth/config.json" + File expectedConfigFile = new ResourceReader().getClasspathResourceAsFile(configFile, AuthConfigReader) + env.indexUrl_v1 >> 'https://index.docker.io/v1/' + dockerConfigReader.getDockerConfigFile() >> expectedConfigFile + + when: + AuthConfig authConfig = authConfigReader.readDefaultAuthConfig() + + then: + 1 * authConfigReader.readAuthConfig(null, expectedConfigFile) + authConfig.serveraddress == "https://index.docker.io/v1/" + authConfig.email == "tobias@gesellix.de" + authConfig.username == "gesellix" + authConfig.password == "-yet-another-password-" + + cleanup: + if (oldDockerConfig) { + System.setProperty("docker.config", oldDockerConfig) + } else { + System.clearProperty("docker.config") + } + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/authentication/CredsStoreHelperIntegrationTest.groovy b/api-client/src/test/groovy/de/gesellix/docker/authentication/CredsStoreHelperIntegrationTest.groovy new file mode 100644 index 00000000..609a4c34 --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/authentication/CredsStoreHelperIntegrationTest.groovy @@ -0,0 +1,138 @@ +package de.gesellix.docker.authentication + +import de.gesellix.docker.engine.DockerEnv +import spock.lang.Requires +import spock.lang.Specification + +class CredsStoreHelperIntegrationTest extends Specification { + + CredsStoreHelper helper + + def setup() { + helper = new CredsStoreHelper() + println "--- ${System.properties['user.name']} on ${System.properties['os.name']}" + } + + @Requires({ System.properties['user.name'] == 'gesellix' && ['Mac OS X', 'Windows'].contains(System.properties['os.name']) }) + def "can get auth from desktop on Mac OS X and Windows"() { + given: + def expected = new CredsStoreHelperResult( + [ + ServerURL: new DockerEnv().indexUrl_v1, + Username : "gesellix", + Secret : "-yet-another-password-" + ] + ) + when: + CredsStoreHelperResult result = helper.getAuthentication("desktop") + then: + result.error == null + result.data.ServerURL == expected.data.ServerURL + result.data.Username == expected.data.Username + result.data.Secret =~ ".+" + } + + @Requires({ System.properties['user.name'] == 'gesellix' && ['Mac OS X', 'Windows'].contains(System.properties['os.name']) }) + def "can list auths from desktop on Mac OS X and Windows"() { + when: + CredsStoreHelperResult result = helper.getAllAuthentications("desktop") + then: + result.error == null + result.data[(new DockerEnv().indexUrl_v1)] == "gesellix" + } + + @Requires({ System.properties['user.name'] == 'gesellix' && System.properties['os.name'] == "Mac OS X" }) + def "can get auth from osxkeychain on Mac OS X"() { + given: + def expected = new CredsStoreHelperResult( + [ + ServerURL: new DockerEnv().indexUrl_v1, + Username : "gesellix", + Secret : "-yet-another-password-" + ] + ) + when: + CredsStoreHelperResult result = helper.getAuthentication("osxkeychain") + then: + result.error == null + result.data.ServerURL == expected.data.ServerURL + result.data.Username == expected.data.Username + result.data.Secret =~ ".+" + } + + @Requires({ System.properties['user.name'] == 'gesellix' && System.properties['os.name'] == "Mac OS X" }) + def "can list auths from osxkeychain on Mac OS X"() { + when: + CredsStoreHelperResult result = helper.getAllAuthentications("osxkeychain") + then: + result.error == null + result.data[(new DockerEnv().indexUrl_v1)] == "gesellix" + } + + @Requires({ System.properties['user.name'] == 'gesellix' && System.properties['os.name'] == "Windows" }) + def "can get auth from wincred on Windows"() { + given: + def expected = new CredsStoreHelperResult( + [ + ServerURL: new DockerEnv().indexUrl_v1, + Username : "gesellix", + Secret : "-yet-another-password-" + ] + ) + when: + CredsStoreHelperResult result = helper.getAuthentication("wincred") + then: + result.error == null + result.data.ServerURL == expected.data.ServerURL + result.data.Username == expected.data.Username + result.data.Secret =~ ".+" + } + + @Requires({ System.properties['user.name'] == 'gesellix' && System.properties['os.name'] == "Windows" }) + def "can list auths from wincred on Windows"() { + when: + CredsStoreHelperResult result = helper.getAllAuthentications("wincred") + then: + result.error == null + result.data[(new DockerEnv().indexUrl_v1)] == "gesellix" + } + + @Requires({ System.properties['user.name'] == 'gesellix' && System.properties['os.name'] == "Linux" }) + def "can get auth from secretservice on Linux"() { + when: + CredsStoreHelperResult result = helper.getAuthentication("secretservice") + then: + result.error == null + result.data.ServerURL == "" + result.data.Username == "gesellix" + result.data.Secret =~ ".+" + } + + @Requires({ System.properties['user.name'] == 'gesellix' && System.properties['os.name'] == "Linux" }) + def "can list auths from secretservice on Linux"() { + when: + CredsStoreHelperResult result = helper.getAllAuthentications("secretservice") + then: + result.error == null + result.data[(new DockerEnv().indexUrl_v1)] == "gesellix" + } + + @Requires({ System.properties['user.name'] == 'gesellix' }) + def "handles errors more or less gracefully"() { + when: + CredsStoreHelperResult result = helper.getAuthentication(System.properties['os.name'] == "Linux" ? "secretservice" : "osxkeychain", "foo") + then: + result.data == null + and: + result.error =~ ".*credentials not found in native keychain.*" + } + + def "handles missing docker-credential-helper more or less gracefully"() { + when: + CredsStoreHelperResult result = helper.getAuthentication("should-be-missing", "foo") + then: + result.data == null + and: + result.error =~ ".*Cannot run program \"docker-credential-should-be-missing\".*" + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/authentication/FileStoreTest.groovy b/api-client/src/test/groovy/de/gesellix/docker/authentication/FileStoreTest.groovy new file mode 100644 index 00000000..b5f8dab3 --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/authentication/FileStoreTest.groovy @@ -0,0 +1,78 @@ +package de.gesellix.docker.authentication + +import spock.lang.Specification + +import static de.gesellix.docker.authentication.AuthConfig.EMPTY_AUTH_CONFIG + +class FileStoreTest extends Specification { + + def "getAuthConfig returns entry matching registry hostname"() { + given: + FileStore credsStore = new FileStore([auths: ["host.name": [auth: "Z2VzZWxsaXg6LXlldC1hbm90aGVyLXBhc3N3b3JkLQ==", email: "tobias@gesellix.de"]]]) + + when: + AuthConfig result = credsStore.getAuthConfig("host.name") + + then: + result == new AuthConfig( + username: "gesellix", + password: "-yet-another-password-", + email: "tobias@gesellix.de", + serveraddress: "host.name" + ) + } + + def "getAuthConfig returns empty AuthConfig for missing entry"() { + given: + FileStore credsStore = new FileStore([auths: ["host.name": [auth: "Z2VzZWxsaXg6LXlldC1hbm90aGVyLXBhc3N3b3JkLQ==", email: "tobias@gesellix.de"]]]) + + expect: + EMPTY_AUTH_CONFIG == credsStore.getAuthConfig("missing.host.name") + } + + def "getAuthConfig returns empty AuthConfig for empty entry"() { + given: + FileStore credsStore = new FileStore([auths: ["host.name": [:]]]) + + expect: + EMPTY_AUTH_CONFIG == credsStore.getAuthConfig("host.name") + } + + def "getAuthConfig returns empty AuthConfig for empty auth"() { + given: + FileStore credsStore = new FileStore([auths: ["host.name": [auth: null, email: "tobias@gesellix.de"]]]) + + expect: + EMPTY_AUTH_CONFIG == credsStore.getAuthConfig("host.name") + } + + def "getAuthConfigs all known AuthConfigs"() { + given: + FileStore credsStore = new FileStore([auths: [ + "host0.name": [identitytoken: "to.k.en"], + "host1.name": [auth: "Z2VzZWxsaXg6LXlldC1hbm90aGVyLXBhc3N3b3JkLQ==", email: "tobias@gesellix.de"], + "host2.name": [auth: "Z2VzZWxsaXg6LWEtcGFzc3dvcmQtZm9yLXF1YXkt", email: "tobias@gesellix.de"]]]) + + when: + Map result = credsStore.getAuthConfigs() + + then: + result.size() == 3 + result["host0.name"] == new AuthConfig( + identitytoken: "to.k.en", + serveraddress: "host0.name" + ) + result["host1.name"] == new AuthConfig( + username: "gesellix", + password: "-yet-another-password-", + email: "tobias@gesellix.de", + serveraddress: "host1.name" + ) + result["host2.name"] == new AuthConfig( + username: "gesellix", + password: "-a-password-for-quay-", + email: "tobias@gesellix.de", + serveraddress: "host2.name" + ) + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/authentication/NativeStoreTest.groovy b/api-client/src/test/groovy/de/gesellix/docker/authentication/NativeStoreTest.groovy new file mode 100644 index 00000000..36ccc069 --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/authentication/NativeStoreTest.groovy @@ -0,0 +1,98 @@ +package de.gesellix.docker.authentication + +import spock.lang.Specification +import spock.lang.Unroll + +import static de.gesellix.docker.authentication.AuthConfig.EMPTY_AUTH_CONFIG + +class NativeStoreTest extends Specification { + + NativeStore credsStore + CredsStoreHelper credsStoreHelper = Mock(CredsStoreHelper) + + def setup() { + credsStore = new NativeStore("test-helper") + credsStore.credsStoreHelper = credsStoreHelper + } + + def "getAuthConfig calls credsStoreHelper"() { + when: + credsStore.getAuthConfig("host.name") + then: + 1 * credsStoreHelper.getAuthentication("test-helper", "host.name") >> [:] + } + + @Unroll + def "getAuthConfig returns empty AuthConfig on invalid credsStoreHelperResponse #credsStoreHelperResponse"() { + given: + credsStoreHelper.getAuthentication("test-helper", "host.name") >> credsStoreHelperResponse + when: + AuthConfig result = credsStore.getAuthConfig("host.name") + then: + result == EMPTY_AUTH_CONFIG + where: + credsStoreHelperResponse << [new CredsStoreHelperResult("for-test"), new CredsStoreHelperResult((String) null)] + } + + def "getAuthConfig returns AuthConfig for valid username/password credentials"() { + given: + def credsStoreHelperResponse = new CredsStoreHelperResult([Username: "foo", Secret: "bar"]) + credsStoreHelper.getAuthentication("test-helper", "host.name") >> credsStoreHelperResponse + + when: + AuthConfig result = credsStore.getAuthConfig("host.name") + + then: + result == new AuthConfig(username: "foo", password: "bar", serveraddress: "host.name") + } + + def "getAuthConfig returns AuthConfig for valid identitytoken credentials"() { + given: + def credsStoreHelperResponse = new CredsStoreHelperResult([Username: "", Secret: "baz"]) + credsStoreHelper.getAuthentication("test-helper", "host.name") >> credsStoreHelperResponse + + when: + AuthConfig result = credsStore.getAuthConfig("host.name") + + then: + result == new AuthConfig(identitytoken: "baz", serveraddress: "host.name") + } + + def "getAuthConfigs calls credsStoreHelper"() { + when: + credsStore.getAuthConfigs() + then: + 1 * credsStoreHelper.getAllAuthentications("test-helper") >> [:] + } + + @Unroll + def "getAuthConfigs returns empty AuthConfig on invalid credsStoreHelperResponse #credsStoreHelperResponse"() { + when: + Map result = credsStore.getAuthConfigs() + + then: + 1 * credsStoreHelper.getAllAuthentications("test-helper") >> new CredsStoreHelperResult(["host1.name": "username", "host2.name": "username"]) + 1 * credsStoreHelper.getAuthentication("test-helper", "host1.name") >> new CredsStoreHelperResult([Username: "", Secret: "baz"]) + 1 * credsStoreHelper.getAuthentication("test-helper", "host2.name") >> credsStoreHelperResponse + 0 * credsStoreHelper._ + result.size() == 2 + result["host2.name"] == EMPTY_AUTH_CONFIG + + where: + credsStoreHelperResponse << [new CredsStoreHelperResult("for-test"), new CredsStoreHelperResult((String) null)] + } + + def "getAuthConfigs returns AuthConfigs for valid credsStoreHelperResponse"() { + when: + Map result = credsStore.getAuthConfigs() + + then: + 1 * credsStoreHelper.getAllAuthentications("test-helper") >> new CredsStoreHelperResult(["host1.name": "username", "host2.name": "username"]) + 1 * credsStoreHelper.getAuthentication("test-helper", "host1.name") >> new CredsStoreHelperResult([Username: "user-name", Secret: "password"]) + 1 * credsStoreHelper.getAuthentication("test-helper", "host2.name") >> new CredsStoreHelperResult([Username: "", Secret: "baz"]) + 0 * credsStoreHelper._ + result.size() == 2 + result["host1.name"] == new AuthConfig(username: "user-name", password: "password", serveraddress: "host1.name") + result["host2.name"] == new AuthConfig(identitytoken: "baz", serveraddress: "host2.name") + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/builder/BuildContextBuilderSpec.groovy b/api-client/src/test/groovy/de/gesellix/docker/builder/BuildContextBuilderSpec.groovy new file mode 100644 index 00000000..5a535c47 --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/builder/BuildContextBuilderSpec.groovy @@ -0,0 +1,100 @@ +package de.gesellix.docker.builder + +import okio.Okio +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream +import spock.lang.Specification + +import java.util.zip.GZIPInputStream + +class BuildContextBuilderSpec extends Specification { + + def "test archiveTarFilesRecursively"() { + given: + def inputDirectory = new File(getClass().getResource("/docker/Dockerfile").toURI()).parentFile + def targetFile = File.createTempFile("buildContext", ".tar") + targetFile.deleteOnExit() + + when: + BuildContextBuilder.archiveTarFilesRecursively(inputDirectory, targetFile) + + then: + def collectedEntryNames = collectEntryNames(targetFile) + collectedEntryNames.sort() == ["subdirectory/payload.txt", "Dockerfile", "script.sh"].sort() + } + + def "test archiveTarFilesRecursively keeps executable flag"() { + given: + def inputDirectory = new File(getClass().getResource("/docker/Dockerfile").toURI()).parentFile + def targetFile = File.createTempFile("buildContext", ".tar") + targetFile.deleteOnExit() + + when: + BuildContextBuilder.archiveTarFilesRecursively(inputDirectory, targetFile) + + then: + getFileMode(targetFile, "script.sh") == 0100755 + } + + def "test archiveTarFilesRecursively excludes targetFile when in same baseDir"() { + given: + def inputDirectory = new File(getClass().getResource("/docker/Dockerfile").toURI()).parentFile + def targetFile = new File(inputDirectory, "buildContext.tar") + targetFile.createNewFile() + targetFile.deleteOnExit() + + when: + BuildContextBuilder.archiveTarFilesRecursively(inputDirectory, targetFile) + + then: + def collectedEntryNames = collectEntryNames(targetFile) + collectedEntryNames.sort() == ["subdirectory/payload.txt", "Dockerfile", "script.sh"].sort() + + // TODO cannot be deleted while the Gradle daemon is running? +// cleanup: +// Files.delete(targetFile.toPath()) +// println targetFile.delete() + } + + def collectEntryNames(File tarArchive) { + def collectedEntryNames = [] + def tarArchiveInputStream = new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(tarArchive))) + + def entry + while (entry = tarArchiveInputStream.nextEntry) { + collectedEntryNames << entry.name + } + collectedEntryNames + } + + def getFileMode(File tarArchive, String filename) { + def tarArchiveInputStream = new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(tarArchive))) + + def entry + while (entry = tarArchiveInputStream.nextEntry) { + if (entry.name == filename) { + return entry.getMode() + } + } + throw new FileNotFoundException(filename) + } + + def "test relativize"() { + when: + def relativized = BuildContextBuilder.relativize(new File("./base/dir"), new File("./base/dir/with/sub/dir")) + + then: + relativized == new File("with/sub/dir").toPath().toString() + } + + def "test copyFile"() { + given: + def inputFile = new File(getClass().getResource("/docker/subdirectory/payload.txt").toURI()) + def outputStream = new ByteArrayOutputStream() + + when: + BuildContextBuilder.copyFile(inputFile, outputStream) + + then: + new String(outputStream.toByteArray()) == Okio.buffer(Okio.source(new FileInputStream(inputFile))).readUtf8() + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/builder/DockerignoreFileFilterSpec.groovy b/api-client/src/test/groovy/de/gesellix/docker/builder/DockerignoreFileFilterSpec.groovy new file mode 100644 index 00000000..74d5c262 --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/builder/DockerignoreFileFilterSpec.groovy @@ -0,0 +1,53 @@ +package de.gesellix.docker.builder + +import org.apache.commons.lang3.SystemUtils +import spock.lang.Requires +import spock.lang.Specification + +class DockerignoreFileFilterSpec extends Specification { + + def "collects desired files"() { + given: + def baseDir = getClass().getResource("/dockerignore").file + def base = new File(baseDir) + when: + def collectedFiles = new DockerignoreFileFilter(base, []).collectFiles(base) + then: + collectedFiles.sort() == [new File("${baseDir}/ignorefolder/keepme.txt"), + new File("${baseDir}/subfolder/content.txt"), + new File("${baseDir}/subfolder/subsubfolder/content.txt")].sort() + } + + def "collects all non-dockerignored files"() { + given: + def baseDir = getClass().getResource("/dockerignore-all-but-some").file + def base = new File(baseDir) + when: + def collectedFiles = new DockerignoreFileFilter(base, []).collectFiles(base) + then: + collectedFiles.sort() == [new File("${baseDir}/Dockerfile"), + new File("${baseDir}/content.txt")].sort() + } + + def "handles trailing slashes in exclude patterns"() { + given: + def baseDir = getClass().getResource("/dockerignore_subdirs").file + def base = new File(baseDir) + when: + def collectedFiles = new DockerignoreFileFilter(base, []).collectFiles(base) + then: + collectedFiles.sort() == [new File("${baseDir}/keepme/a-file-to-be-kept.txt"), + new File("${baseDir}/keepme/subdir/keep-me.txt")].sort() + } + + @Requires({ SystemUtils.IS_OS_WINDOWS }) + def "handles trailing backslashes in patterns (windows only)"() { + given: + def baseDir = getClass().getResource("/dockerignore_windows").file + def base = new File(baseDir) + when: + def collectedFiles = new DockerignoreFileFilter(base, []).collectFiles(base) + then: + collectedFiles == [new File("${baseDir}/keepme/to-be-kept.txt")] + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/builder/GlobsMatcherSpec.groovy b/api-client/src/test/groovy/de/gesellix/docker/builder/GlobsMatcherSpec.groovy new file mode 100644 index 00000000..5891a3ad --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/builder/GlobsMatcherSpec.groovy @@ -0,0 +1,164 @@ +package de.gesellix.docker.builder + +import org.apache.commons.lang3.SystemUtils +import spock.lang.Requires +import spock.lang.Specification +import spock.lang.Unroll + +import java.util.regex.PatternSyntaxException + +class GlobsMatcherSpec extends Specification { + + def "matches all patterns"() { + given: + def matcher = new GlobsMatcher(new File(""), ["abc", "cde"]) + matcher.initMatchers() + + expect: + matcher.matchers.size() == 2 + and: + matcher.matches(new File("cde")) + and: + matcher.matches(new File("abc")) + } + + @Unroll + "#pattern should match #path"(String pattern, File base, File path) { + expect: + new GlobsMatcher(base, [pattern]).matches(path) + + where: + pattern | base | path + "abc" | new File(".") | new File("./abc") + "abc" | new File("") | new File("abc") + "*" | new File("") | new File("abc") + "*c" | new File("") | new File("abc") + "a*" | new File("") | new File("a/bc") + "a*/b" | new File("") | new File("a/b/c") + "a*" | new File("") | new File("a") + "a*/b" | new File("") | new File("abc/b") + "a*b*c*d*e*/f" | new File("") | new File("axbxcxdxe/f") + "a*b*c*d*e*/f" | new File("") | new File("axbxcxdxexx/f") + "a*b?c*x" | new File("") | new File("abxbbxdbxebxczzx") + "ab[c]" | new File("") | new File("abc") + "ab[b-d]" | new File("") | new File("abc") + "ab[!c]" | new File("") | new File("abd") + "ab[!e-g]" | new File("") | new File("abc") + "a?c" | new File("") | new File("a§c") + "[a-ζ]*" | new File("") | new File("α") + "[-]" | new File("") | new File("-") + } + + @Requires({ SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC }) + @Unroll + "#pattern should match #path on unix systems"(String pattern, File base, File path) { + expect: + new GlobsMatcher(base, [pattern]).matches(path) + + where: + pattern | base | path + "[\\-]" | new File("") | new File("-") + "[x\\-]" | new File("") | new File("-") + "[x\\-]" | new File("") | new File("x") + "[\\-x]" | new File("") | new File("x") +// "[\\]a]" | new File("") | new File("]") + } + + @Requires({ SystemUtils.IS_OS_WINDOWS }) + @Unroll + "#pattern should match #path on windows systems"(String pattern, File base, File path) { + expect: + new GlobsMatcher(base, [pattern]).matches(path) + + where: + pattern | base | path + "bin\\" | new File("") | new File("bin\\foo") + } + + @Unroll + "#pattern should not match #path"(String pattern, File path) { + expect: + !new GlobsMatcher(new File(""), [pattern]).matches(path) + + where: + pattern | path + "a*/b" | new File("a/c/b") + "a*b*c*d*e*/f" | new File("axbxcxdxe/xx/f") + "a*b*c*d*e*/f" | new File("axbxcxdxexx/ff") + "a*b?c*x" | new File("abxbbxdbxebxczzy") + "ab[e-g]" | new File("abc") + "ab[!c]" | new File("abc") + "ab[!b-d]" | new File("abc") + "a??c" | new File("abc") + "!a*b" | new File("ab") + "a?b" | new File("a/b") + "a*b" | new File("a/b") + } + + @Requires({ SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC }) + @Unroll + "#pattern should not match #path on unix systems"(String pattern, File base, File path) { + expect: + !new GlobsMatcher(base, [pattern]).matches(path) + + where: + pattern | base | path + "[a-ζ]*" | new File("") | new File("A") + "[x\\-]" | new File("") | new File("z") + "[\\-x]" | new File("") | new File("z") + } + + @Unroll + "#pattern should throw exception"(String pattern, File base, File path) { + when: + new GlobsMatcher(base, [pattern]).matches(path) + + then: + thrown(PatternSyntaxException) + + where: + pattern | base | path + "[]a]" | new File("") | new File("]") + } + + @Requires({ SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC }) + @Unroll + "#pattern should throw exception on unix systems"(String pattern, File base, File path) { + when: + new GlobsMatcher(base, [pattern]).matches(path) + + then: + thrown(PatternSyntaxException) + + where: + pattern | base | path + // On *nix, the backslash is the escape character, so it's invalid to be used at the end of a pattern + // A trailing backslash is only valid on Windows. + // We actually differ from the official client's behaviour, which silently ignores invalid patterns. + "bin\\" | new File("") | new File("bin/foo") + } + + def "allows pattern exclusions"() { + expect: + new GlobsMatcher(new File(""), patterns).matches(path) == shouldMatch + + where: + patterns | path | shouldMatch + ["!ab.c", "*.c"] | new File("ab.c") | true + ["dir", "!dir/ab.c"] | new File("dir/ab.c") | false + ["dir/", "!dir/ab.c"] | new File("dir/ab.c") | false + ["dir/*", "!dir/ab.c"] | new File("dir/ab.c") | false + ["dir/*.c"] | new File("dir/ab.c") | true + ["dir/*.c", "!dir/ab.c"] | new File("dir/ab.c") | false + } + + def "allows pattern exclusions in subdirectories"() { + expect: + new GlobsMatcher(new File(""), patterns).matches(path) == shouldMatch + + where: + patterns | path | shouldMatch + ["ignorefolder", "!ignorefolder/keepme.txt", "**/ignore.txt"] | new File("ignorefolder/keepme.txt") | false + ["ignorefolder", "!ignorefolder/keepme.txt", "**/ignore.txt"] | new File("ignorefolder/dropme.txt") | true + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/context/DockerContextResolverTest.groovy b/api-client/src/test/groovy/de/gesellix/docker/context/DockerContextResolverTest.groovy new file mode 100644 index 00000000..61ede4b8 --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/context/DockerContextResolverTest.groovy @@ -0,0 +1,40 @@ +package de.gesellix.docker.context + +import de.gesellix.docker.engine.DockerConfigReader +import de.gesellix.testutil.ResourceReader +import spock.lang.Specification + +class DockerContextResolverTest extends Specification { + + private DockerContextResolver dockerContextResolver + + def setup() { + dockerContextResolver = new DockerContextResolver() + } + + def "resolve context"() { + given: + File configFile = new ResourceReader().getClasspathResourceAsFile('/context/config.json', DockerContextResolver) + DockerConfigReader reader = new DockerConfigReader() + reader.dockerConfigFile = configFile + + when: + String contextName = dockerContextResolver.resolveDockerContextName(reader) + + then: + contextName == "for-test" + } + + def "resolve endpoint"() { + given: + File configFile = new ResourceReader().getClasspathResourceAsFile('/context/config.json', DockerContextResolver) + File dockerContextStoreDir = new File(configFile.getParentFile(), "contexts"); + ContextStore contextStore = new ContextStore(dockerContextStoreDir) + + when: + EndpointMetaBase endpoint = dockerContextResolver.resolveDockerEndpoint(contextStore, "for-test") + + then: + endpoint.host == "unix:///var/run/docker.sock" + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/context/MetadataStoreTest.groovy b/api-client/src/test/groovy/de/gesellix/docker/context/MetadataStoreTest.groovy new file mode 100644 index 00000000..f0ce3493 --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/context/MetadataStoreTest.groovy @@ -0,0 +1,41 @@ +package de.gesellix.docker.context + +import de.gesellix.testutil.ResourceReader +import spock.lang.Specification +import spock.lang.Unroll + +class MetadataStoreTest extends Specification { + private MetadataStore store + + def setup() { + File configFile = new ResourceReader().getClasspathResourceAsFile('/context/config.json', MetadataStore) + File dockerContextStoreDir = new File(configFile.getParentFile(), "contexts") + store = new MetadataStore(new File(dockerContextStoreDir, MetadataStore.metadataDir)) + } + + @Unroll + def "should hex-encode the SHA-256 digest of '#contextName' to '#contextDir'"() { + when: + String directoryName = store.getContextDir(contextName) + + then: + directoryName == contextDir + + where: + contextName | contextDir + "for-test" | "297dc204469307b573ca1e71dead5336f61c3aa222bf3a507cd59bf0c07a43b8" + "desktop-linux" | "fe9c6bd7a66301f49ca9b6a70b217107cd1284598bfc254700c989b916da791e" + } + + def "should read metadata"() { + when: + Metadata metadata = store.getMetadata("for-test") + + then: + metadata.name == "for-test" + metadata.metadata == [:] + metadata.endpoints == [ + docker: new EndpointMetaBase("unix:///var/run/docker.sock", false) + ] + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/engine/DockerClientConfigSpec.groovy b/api-client/src/test/groovy/de/gesellix/docker/engine/DockerClientConfigSpec.groovy new file mode 100644 index 00000000..16eb6d90 --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/engine/DockerClientConfigSpec.groovy @@ -0,0 +1,188 @@ +package de.gesellix.docker.engine + +import spock.lang.Specification +import spock.lang.Unroll + +import java.nio.file.Files +import java.nio.file.Paths + +class DockerClientConfigSpec extends Specification { + + @Unroll + def "should assume TLS when tlsVerify==#tlsVerify"() { + def certsDir = Files.createTempDirectory("certs") + def dockerEnv = new DockerEnv( + tlsVerify: tlsVerify, + certPath: certsDir.toString()) + def dockerClientConfig = new DockerClientConfig(dockerEnv) + when: + def tlsConfig = dockerClientConfig.getTlsConfig(new URL("https://example.com:2376"), dockerEnv) + then: + tlsConfig.tlsVerify + and: + tlsConfig.certPath == certsDir.toString() + where: + // yes, even the falsy values (0, false, no) actually enable tls-verify + tlsVerify << ["1", "true", "TRUE", "yes", "YES", "0", "false", "FALSE", "no", "NO"] + } + + @Unroll + def "should not assume TLS when tlsVerify==#tlsVerify"() { + def certsDir = Files.createTempDirectory("certs") + def dockerEnv = new DockerEnv( + tlsVerify: tlsVerify, + certPath: certsDir.toString()) + def dockerClientConfig = new DockerClientConfig(dockerEnv) + when: + def assumeTls = dockerClientConfig.getTlsConfig(new URL("https://example.com:2376"), dockerEnv).tlsVerify + then: + !assumeTls + and: + dockerClientConfig.certPath == null + where: + tlsVerify << [""] + } + + def "should not assume TLS when port !== 2376"() { + def dockerEnv = new DockerEnv( + tlsVerify: null, + certPath: "/some/non-existing/path") + def dockerClientConfig = new DockerClientConfig(dockerEnv) + when: + def assumeTls = dockerClientConfig.getTlsConfig(new URL("https://example.com:2375"), dockerEnv).tlsVerify + then: + !assumeTls + and: + dockerClientConfig.certPath == null + } + + def "should fail when tlsVerify=1, but certs directory doesn't exist"() { + def dockerEnv = new DockerEnv( + tlsVerify: "1", + certPath: "/some/non-existing/path", + defaultCertPath: new File("/some/non-existing/default/path")) + def dockerClientConfig = new DockerClientConfig(dockerEnv) + when: + dockerClientConfig.getTlsConfig(new URL("https://example.com:2375"), dockerEnv) + then: + thrown(IllegalStateException) + } + + def "should try to use the default .docker cert path"() { + def dockerEnv = new DockerEnv( + tlsVerify: null, + certPath: "/some/non-existing/path") + def defaultDockerCertPathExisted = Files.exists(Paths.get(dockerEnv.defaultCertPath)) + if (!defaultDockerCertPathExisted) { + Files.createDirectory(Paths.get(dockerEnv.defaultCertPath)) + } + def dockerClientConfig = new DockerClientConfig(dockerEnv) + + when: + def tlsConfig = dockerClientConfig.getTlsConfig(new URL("https://example.com:2376"), dockerEnv) + + then: + tlsConfig.tlsVerify + and: + tlsConfig.certPath == dockerEnv.defaultCertPath + + cleanup: + defaultDockerCertPathExisted || Files.delete(Paths.get(dockerEnv.defaultCertPath)) + } + + def "should choose http for 'tcp://127.0.0.1:2375'"() { + def dockerEnv = new DockerEnv(dockerHost: "tcp://127.0.0.1:2375", tlsVerify: null) + when: + def dockerClientConfig = new DockerClientConfig(dockerEnv) + then: + dockerClientConfig.scheme == "http" + dockerClientConfig.host == "127.0.0.1" + dockerClientConfig.port == 2375 + dockerClientConfig.certPath == null + } + + def "should choose http for 'http://127.0.0.1:2375'"() { + def dockerEnv = new DockerEnv(dockerHost: "http://127.0.0.1:2375", tlsVerify: null) + when: + def dockerClientConfig = new DockerClientConfig(dockerEnv) + then: + dockerClientConfig.scheme == "http" + dockerClientConfig.host == "127.0.0.1" + dockerClientConfig.port == 2375 + dockerClientConfig.certPath == null + } + + def "should choose http for 'https://127.0.0.1:2376' and disabled tls"() { + def dockerEnv = new DockerEnv(dockerHost: "https://127.0.0.1:2376", tlsVerify: "") + when: + def dockerClientConfig = new DockerClientConfig(dockerEnv) + then: + dockerClientConfig.scheme == "http" + dockerClientConfig.host == "127.0.0.1" + dockerClientConfig.port == 2376 + dockerClientConfig.certPath == null + } + + def "should choose https for 'https://127.0.0.1:2376' and enabled tls"() { + def certsDir = Files.createTempDirectory("certs") + def dockerEnv = new DockerEnv(dockerHost: "https://127.0.0.1:2376", tlsVerify: "1", certPath: certsDir.toString()) + when: + def dockerClientConfig = new DockerClientConfig(dockerEnv) + then: + dockerClientConfig.scheme == "https" + dockerClientConfig.host == "127.0.0.1" + dockerClientConfig.port == 2376 + dockerClientConfig.certPath == certsDir.toString() + } + + def "should choose unix socket for 'unix:///var/run/socket.example'"() { + when: + def dockerClientConfig = new DockerClientConfig("unix:///var/run/socket.example") + then: + dockerClientConfig.scheme == "unix" + dockerClientConfig.host == "/var/run/socket.example" + dockerClientConfig.port == -1 + dockerClientConfig.certPath == null + } + + def "should choose named pipe for 'npipe:////./pipe/docker_engine'"() { + when: + def dockerClientConfig = new DockerClientConfig("npipe:////./pipe/docker_engine") + then: + dockerClientConfig.scheme == "npipe" + dockerClientConfig.host == "//./pipe/docker_engine" + dockerClientConfig.port == -1 + dockerClientConfig.certPath == null + } + + def "should ignore unknown protocol"() { + when: + def dockerClientConfig = new DockerClientConfig("ftp://example/foo") + then: + dockerClientConfig.scheme == "ftp" + dockerClientConfig.host == "example" + dockerClientConfig.port == -1 + dockerClientConfig.certPath == null + } + + @Unroll + def "should assume content trust when contentTrust==#contentTrust"() { + def dockerEnv = new DockerEnv(dockerContentTrust: contentTrust) + def dockerClientConfig = new DockerClientConfig(dockerEnv) + expect: + dockerClientConfig.isContentTrustEnabled(dockerEnv) + where: + // yes, even the invalid values (foo, bar) actually enable content trust + contentTrust << ["1", "true", "TRUE", "yes", "YES", "f00"] + } + + @Unroll + def "should not assume content trust when contentTrust==#contentTrust"() { + def dockerEnv = new DockerEnv(dockerContentTrust: contentTrust) + def dockerClientConfig = new DockerClientConfig(dockerEnv) + expect: + !dockerClientConfig.isContentTrustEnabled(dockerEnv) + where: + contentTrust << ["", "0", "false", "FALSE", "no", "NO"] + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/engine/DockerConfigReaderTest.groovy b/api-client/src/test/groovy/de/gesellix/docker/engine/DockerConfigReaderTest.groovy new file mode 100644 index 00000000..9d228ade --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/engine/DockerConfigReaderTest.groovy @@ -0,0 +1,26 @@ +package de.gesellix.docker.engine + +import de.gesellix.docker.context.DockerContextResolver +import de.gesellix.testutil.ResourceReader +import spock.lang.Specification + +class DockerConfigReaderTest extends Specification { + + private DockerConfigReader reader + + def setup() { + reader = new DockerConfigReader() + } + + def "reads the Docker config file"() { + given: + File configFile = new ResourceReader().getClasspathResourceAsFile('/context/config.json', DockerContextResolver) + + when: + Map configFileContent = reader.readDockerConfigFile(configFile) + + then: + configFileContent.get("credsStore") == "desktop" + configFileContent.get("currentContext") == "for-test" + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/engine/DockerEnvTest.groovy b/api-client/src/test/groovy/de/gesellix/docker/engine/DockerEnvTest.groovy new file mode 100644 index 00000000..c2a8e0d5 --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/engine/DockerEnvTest.groovy @@ -0,0 +1,75 @@ +package de.gesellix.docker.engine + +import de.gesellix.testutil.ResourceReader +import spock.lang.Specification + +class DockerEnvTest extends Specification { + + def "read configured docker config.json"() { + given: + def expectedConfigDir = new File('.').absoluteFile + def oldDockerConfigDir = System.setProperty("docker.config", expectedConfigDir.absolutePath) + DockerEnv env = new DockerEnv() + + when: + def dockerConfigFile = env.getDockerConfigFile() + + then: + dockerConfigFile.absolutePath == new File(expectedConfigDir, 'config.json').absolutePath + + cleanup: + if (oldDockerConfigDir) { + System.setProperty("docker.config", oldDockerConfigDir) + } else { + System.clearProperty("docker.config") + } + } + + def "read default docker config file"() { + given: + def expectedConfigFile = new ResourceReader().getClasspathResourceAsFile('/auth/config.json', getClass()) + def oldDockerConfigDir = System.setProperty("docker.config", expectedConfigFile.parent) + DockerEnv env = new DockerEnv() + + when: + File actualConfigFile = env.getDockerConfigFile() + + then: + actualConfigFile == expectedConfigFile + + cleanup: + if (oldDockerConfigDir) { + System.setProperty("docker.config", oldDockerConfigDir) + } else { + System.clearProperty("docker.config") + } + } + + def "read legacy docker config file"() { + given: + DockerEnv env = new DockerEnv() + def oldDockerConfig = System.clearProperty("docker.config") + + def nonExistingFile = new File('./I should not exist') + assert !nonExistingFile.exists() + env.dockerConfigReader.configFile = nonExistingFile + + def expectedConfigFile = new ResourceReader().getClasspathResourceAsFile('/auth/dockercfg', getClass()) + env.dockerConfigReader.legacyConfigFile = expectedConfigFile + + env.resetDockerHostFromCurrentConfig() + + when: + def actualConfigFile = env.getDockerConfigFile() + + then: + actualConfigFile == expectedConfigFile + + cleanup: + if (oldDockerConfig) { + System.setProperty("docker.config", oldDockerConfig) + } else { + System.clearProperty("docker.config") + } + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/engine/DockerVersionSpec.groovy b/api-client/src/test/groovy/de/gesellix/docker/engine/DockerVersionSpec.groovy new file mode 100644 index 00000000..3240cc6d --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/engine/DockerVersionSpec.groovy @@ -0,0 +1,45 @@ +package de.gesellix.docker.engine + +import spock.lang.Specification +import spock.lang.Unroll + +import static de.gesellix.docker.engine.DockerVersion.parseDockerVersion + +class DockerVersionSpec extends Specification { + + def "fails for invalid version"() { + given: + // this pattern appears in the wild for packages installed from + // https://master.dockerproject.org/ + String versionString = "master-dockerproject-2022-03-26" + + when: + parseDockerVersion(versionString) + + then: + def e = thrown(IllegalArgumentException) + e.message == "Version does not match the expected version pattern: '$versionString'" + } + + @Unroll + def "parse version #versionString"() { + expect: + parseDockerVersion(versionString) == version + where: + versionString | version + "1.12.0" | new DockerVersion(major: 1, minor: 12, patch: 0, meta: "") + "1.12.0-rc2" | new DockerVersion(major: 1, minor: 12, patch: 0, meta: "-rc2") + "17.03.0-ce-rc1" | new DockerVersion(major: 17, minor: 3, patch: 0, meta: "-ce-rc1") + } + + @Unroll + def "compare #v1 with #v2"() { + expect: + parseDockerVersion(v1) <=> parseDockerVersion(v2) == result + where: + v1 | v2 || result + "1.13" | "17.04" || -1 + "17.05" | "17.04" || 1 + "17.05" | "17.05" || 0 + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/json/CustomObjectAdapterFactoryTest.groovy b/api-client/src/test/groovy/de/gesellix/docker/json/CustomObjectAdapterFactoryTest.groovy new file mode 100644 index 00000000..1c2579d2 --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/json/CustomObjectAdapterFactoryTest.groovy @@ -0,0 +1,35 @@ +package de.gesellix.docker.json + +import com.squareup.moshi.Moshi +import spock.lang.Specification +import spock.lang.Unroll + +class CustomObjectAdapterFactoryTest extends Specification { + + Moshi moshi + + def setup() { + moshi = new Moshi.Builder().add(new CustomObjectAdapterFactory()).build() + } + + def "should deserialize numeric"() { + when: + def result = moshi.adapter(Map).fromJson('{"number":1.2}') + then: + result.number == 1.2 + } + + @Unroll + def "should serialize BigDecimal with value #number"() { + when: + def result = moshi.adapter(Map).toJson([number: new BigDecimal("$number")]) + then: + result == "{\"number\":$expectedValue}" + where: + number | expectedValue + 0 | "0" + 1.2 | "1.2" + 1 | "1" + 0.000001 | "0.000001" + } +} diff --git a/api-client/src/test/groovy/de/gesellix/docker/ssl/KeyStoreUtilTest.groovy b/api-client/src/test/groovy/de/gesellix/docker/ssl/KeyStoreUtilTest.groovy new file mode 100644 index 00000000..ec9e27fa --- /dev/null +++ b/api-client/src/test/groovy/de/gesellix/docker/ssl/KeyStoreUtilTest.groovy @@ -0,0 +1,54 @@ +package de.gesellix.docker.ssl + +import spock.lang.Specification + +import java.security.GeneralSecurityException +import java.security.KeyStore + +class KeyStoreUtilTest extends Specification { + + def "can create KeyStore from RSA certs directory"() { + when: + KeyStore keyStore = KeyStoreUtil.createDockerKeyStore(getFile("algorithm/RSA/certpath")) + KeyStore.PrivateKeyEntry pkEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry( + "docker", + new KeyStore.PasswordProtection(KeyStoreUtil.KEY_STORE_PASSWORD)) + then: + pkEntry.certificate != null + keyStore.getCertificate("cn=ca-test,o=internet widgits pty ltd,st=some-state,c=cr") + keyStore.getCertificate("cn=ca-test-2,o=internet widgits pty ltd,st=some-state,c=cr") + } + + def "can load RSA/PKCS1"() { + expect: + KeyStoreUtil.loadPrivateKey(getFile("algorithm/RSA/keys/pkcs1.pem")) != null + } + + def "can load RSA/PKCS8"() { + expect: + KeyStoreUtil.loadPrivateKey(getFile("algorithm/RSA/keys/pkcs8.pem")) != null + } + + def "can load ECDSA"() { + expect: + KeyStoreUtil.loadPrivateKey(getFile("algorithm/ECDSA/keys/ecdsa.pem")) != null + } + + def "can load ECDSA (skip params)"() { + // created via `openssl ecparam -name secp521r1 -genkey -param_enc explicit -out ecdsa_with_params.pem` + expect: + KeyStoreUtil.loadPrivateKey(getFile("algorithm/ECDSA/keys/ecdsa_with_params.pem")) != null + } + + def "cannot load invalid key"() { + when: + KeyStoreUtil.loadPrivateKey(getFile("algorithm/RSA/keys/invalid.pem")) + then: + def exception = thrown(GeneralSecurityException) + exception.message =~ "Cannot generate private key .*" + } + + static String getFile(String path) { + KeyStoreUtilTest.class.getResource(path).getFile() + } +} diff --git a/api-client/src/test/resources/auth/config.json b/api-client/src/test/resources/auth/config.json new file mode 100644 index 00000000..b9c5af23 --- /dev/null +++ b/api-client/src/test/resources/auth/config.json @@ -0,0 +1,12 @@ +{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "Z2VzZWxsaXg6LXlldC1hbm90aGVyLXBhc3N3b3JkLQ==", + "email": "tobias@gesellix.de" + }, + "quay.io": { + "auth": "Z2VzZWxsaXg6LWEtcGFzc3dvcmQtZm9yLXF1YXkt", + "email": "tobias@gesellix.de" + } + } +} diff --git a/api-client/src/test/resources/auth/dockercfg b/api-client/src/test/resources/auth/dockercfg new file mode 100644 index 00000000..e1b34fb3 --- /dev/null +++ b/api-client/src/test/resources/auth/dockercfg @@ -0,0 +1,10 @@ +{ + "https://index.docker.io/v1/": { + "auth": "Z2VzZWxsaXg6LXlldC1hbm90aGVyLXBhc3N3b3JkLQ", + "email": "tobias@gesellix.de" + }, + "quay.io": { + "auth": "Z2VzZWxsaXg6LWEtcGFzc3dvcmQtZm9yLXF1YXkt", + "email": "tobias@gesellix.de" + } +} diff --git a/api-client/src/test/resources/auth/dockercfg-with-credsStore-Linux b/api-client/src/test/resources/auth/dockercfg-with-credsStore-Linux new file mode 100644 index 00000000..a0b82fda --- /dev/null +++ b/api-client/src/test/resources/auth/dockercfg-with-credsStore-Linux @@ -0,0 +1,6 @@ +{ + "auths": { + "https://index.docker.io/v1/": {} + }, + "credsStore": "secretservice" +} diff --git a/api-client/src/test/resources/auth/dockercfg-with-credsStore-Mac_os_x b/api-client/src/test/resources/auth/dockercfg-with-credsStore-Mac_os_x new file mode 100644 index 00000000..8b67fa92 --- /dev/null +++ b/api-client/src/test/resources/auth/dockercfg-with-credsStore-Mac_os_x @@ -0,0 +1,6 @@ +{ + "auths": { + "https://index.docker.io/v1/": {} + }, + "credsStore": "desktop" +} diff --git a/api-client/src/test/resources/certs/ca-key.pem b/api-client/src/test/resources/certs/ca-key.pem new file mode 100644 index 00000000..d3bcf9e1 --- /dev/null +++ b/api-client/src/test/resources/certs/ca-key.pem @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,6E593A5BFD572C2B + +08bzYkmE7naFSbYQJcRgoNQzncHTotRyqwja2sIsrBKdjIkFPo3DorX+RSUa51Gy +GHAqzPPMhKmgYhQ9l9DeXaF39zYg4qtA+Flm47dS10Ag1Ld7s8PT8CFBCq1OPStI +Co/WEddtvAQB5LjGcuqPNlC8z2GbniH+22i3wEWKS4wuYnvGOvgMLU3bqQqmOAkZ +WpeHe4EcV1fRiKedhsVr9gxaA8rfhueD6JRzJvtrfwxU3YGiHOfsVInRHY+y9GGD +QqSCSXFuvp9zV97P1rHOoW6Q8NwS4QW1zefTZlHxTu6S7KZxiI2smGprsOrQSoI0 +vEqqBzCxelMmwMwRbUCECv3Z/t2ZF1guffYv2Lkbxy1qd7QBchm2nGKYHHAoGbpJ +m2NXh8BssSXT2jQFSt1IrJx+h5wRwJN/Zf5Dquf/yrW0Pk6nJU9mQn1gD0qa80Cl +D2sk+Hg2NwbzEED4VVAwSXVcGAuIufq358CY8NMnXoN5zS8r4HjOu55wwllay0jF +F9TEnDfsoMDbKoOazk+aywU6M51hn/MQnOoRFYUftogJGIEOUbIe0YhDOlWpO3Z0 +8cAjEvOrv8BWbOqVr+FRcFKBAL2L8ONnK390UwsWG+uej6ZNIFAwMMcbPYQV/lem +TuukzSp+/1zHKpgKas+alkTXGCri+B5AVsLxMOxgqQofni5yNcUuFTQoQVrkKSOx +HF7wr2p1ycelRqdSI4kUUWwvDuh9/R+Lo33Zoxf2jxmB9+Omvs46NFrcxt137Xbp +mKoeg9cevJGMhJ7Iu7apjTrThFdq3Df5F1EwGEA4AlvjK+V59Mj5o7s6EpCDQJTQ +u6xcrIYFHRYgAADYRMxIyTd36kPk3DOp/9XtnHsxm9Gluy2bNMfpg3JZucalXY5v +nUvdtudkGsyz4eDqJn5zi7AtzcBPrxw3t0lEeIa5M74GKrRFhSVJkbmMNzRbs60a +TPpfHlkwVGgVse+2TMu4TIuWOa3JMEg85FheGtlRAF0j3BRAO1RVGr4U8fudEwSA +vYTcWN/wvFTR+yb5uMVK0OR1oe9E9gSJI8CoiGR4B+R8CxV6lvrMkDVclNgKPNYL ++90Axvbw+SksKWu7JKImOhb8FQddAUro13D5vEoQsj92Q6QwWylWpokOeu4Mr/cx +WGpxk4MS3tXEOmO28bP1pD+oNPM1ErhFgk6fVIogDJrkskaL4HM63vul3TxFoNWr +WXI4hp6XPhrZcZG2aBGKCN87qDHD2BcOr3ya2RQgbIIs/fyijtaNk0/RjJeKQ3Jb +shQNyD7Hwg5UfPJWdkQYek57iK+0z/GlkPk+jhbLaW79VGbNjVjlKyx3sKUlXgpS +E8OjJPWyX0aL3F8EFKvDsNb4ZKF+f0+weS1exbSNPNsM36m0gaGjQXHFXZsL3fGP +MhAEfUTKVfjFKUY8Tkzmlud0paFp/nw9lf38VZU4+CFx8dGzcnGKPWKz+F7shHdO +QjD7B/XJgVCcfCIIw+rBHhBULJd2lOwHwcLMLIW4pIaSM3Bjntz4tFsGWd5JTCQL +hT/oC0GkDZ9OfRaOi66NGt90S1Oye37y4Ipr7XadXFkObBQO+0j9Rg== +-----END RSA PRIVATE KEY----- diff --git a/api-client/src/test/resources/certs/ca.pem b/api-client/src/test/resources/certs/ca.pem new file mode 100644 index 00000000..6ad23321 --- /dev/null +++ b/api-client/src/test/resources/certs/ca.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDjzCCAnegAwIBAgIJAIRNFM4B9kg3MA0GCSqGSIb3DQEBCwUAMF4xCzAJBgNV +BAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjERMA8GA1UE +CgwIZ2VzZWxsaXgxGjAYBgNVBAMMEVRvYmlhcyBHZXNlbGxjaGVuMB4XDTE1MDEw +MzE0MjQwN1oXDTE2MDEwMzE0MjQwN1owXjELMAkGA1UEBhMCREUxDzANBgNVBAgM +BkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMREwDwYDVQQKDAhnZXNlbGxpeDEaMBgG +A1UEAwwRVG9iaWFzIEdlc2VsbGNoZW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQC+ujOwDDbsSGHFCYluYXlt1VRVH/Tgwjvm//nqSKh66LSHyRIp2lMW +0GnnODQO/N3y0RsOsd14mUj/KeCxlk3Lew0uJOVM2F1kAlqbCkJY2lqFvEJD62lp +eT2JH0011KGaiAgDN7/u8vt8Qf8QuqIKfPm4av16piMuBorIdC6yCwYY0FG8CY9K +5TpQntdQU4i1/tPtYT9zdlB8Kk9Qz6kcpcxtLd/FY044ff9CAVZRxnV/ofvGB0Tg +rrVxd1WrIJDe5IXAGp8zdKBm/yhlSsHd6/Z1BtEkZVHmd3ThA64ZVfHMzZM7xMSp +aCvfuQ9LyXcQobP2tC2S+SjihnuerSkBAgMBAAGjUDBOMB0GA1UdDgQWBBTVpw0K +Ye5NsRoqqFqrXp1/vqPRgjAfBgNVHSMEGDAWgBTVpw0KYe5NsRoqqFqrXp1/vqPR +gjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAKDaeb4oUYMVLdHwSn +bthl1nImt1VISW1sIC1L30/TnA/TG+zhDt9nj2CMCNBXLZlVJw5fNyHc1+zMJ3r4 +c/w7LQbfUIv8B9kezOlfQNEvErQgrPm3Ci0gD3wYFpcEG9FPkeVyYhuWAVNfN696 +IwTRYMq54lNIW17Rf3lGbRpbVN/2+vSr+xpw1tJMwMEn2QBC51Gju/3b2Pgnh5ip +k15f1JEbgyoxCNwB289yK6chE9n6XQ+yX9f86QJ4Q39CA/nOy9/ausTCOZyk39L9 +oIzV40XpsOxns0ynBIh9ljlkJB4SZi1e/vZanJtFvXrundS3Kl/45HJWpD60hwzx +YzCy +-----END CERTIFICATE----- diff --git a/api-client/src/test/resources/certs/ca.srl b/api-client/src/test/resources/certs/ca.srl new file mode 100644 index 00000000..75016ea3 --- /dev/null +++ b/api-client/src/test/resources/certs/ca.srl @@ -0,0 +1 @@ +03 diff --git a/api-client/src/test/resources/certs/cert.pem b/api-client/src/test/resources/certs/cert.pem new file mode 100644 index 00000000..ddb3e7a5 --- /dev/null +++ b/api-client/src/test/resources/certs/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDATCCAemgAwIBAgIBAzANBgkqhkiG9w0BAQsFADBeMQswCQYDVQQGEwJERTEP +MA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xETAPBgNVBAoMCGdlc2Vs +bGl4MRowGAYDVQQDDBFUb2JpYXMgR2VzZWxsY2hlbjAeFw0xNTAxMDMxNDI2MzNa +Fw0xNjAxMDMxNDI2MzNaMBExDzANBgNVBAMMBmNsaWVudDCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKb+grFVBKmLTHvouteZm4kbhwKiXG2VWRvf9zQo +oDqJFk7R6PUaOakHFbOKnEJfUr/RjnyizxV91m0qlsJijA9WR4yt05RzxLvHEpLk +4dltoQFNqGLQ30pkUuVWhm6qx9WXk/xN7VTpqsqjb1aDGtRKKJQGyIP1BPmWk3Bg +4I/73QANCKxTmeMLobG8L50t9Y/phTD1TlWAwUYjgaRPfCzcUSj+XcFGSfFDLphm +wo8O8GW3w4kYG7Dk4dwBZfakfv7RikSlhR3jQ29USnJVzoPqmhPPR6cSj+uaQSPj +CaQi38b7kTADo/LW7Sr9wh59H8rO45GD6HQghZ920spYLHkCAwEAAaMXMBUwEwYD +VR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggEBAJl7PCB+LyEHbrVx +Bip+rAH6JtxSY86vhSHUw1+CBjvG57Lu1SvyLllk4Lt9+qoY3VM3FEOP0mE4Bsk3 +FoqgmN1g/gfcniRNZWqEblZKrZaqanbsl3S3di4svscsGvsXW+ckb/tt5oi4qoDf +cpNrKIliaNNCeNRonqlp2a2SN6NP8VyQLt2gYZ8Vui7mlxVzlHmk3H8nDwLL6VT1 +GmAut+8zbwv1hssIEzLANjgtJhnMnMEgYZyfE7nE0bSivaBaKDNwLLXpAnll70/m ++LQiDuF7V4SOZoGfOklwdKa4kDVszu5Q4Iz4kNoddFZLNVZkcs//Lp2LIqrzMqeD +RCfz8XE= +-----END CERTIFICATE----- diff --git a/api-client/src/test/resources/certs/client.csr b/api-client/src/test/resources/certs/client.csr new file mode 100644 index 00000000..0c4fc8d1 --- /dev/null +++ b/api-client/src/test/resources/certs/client.csr @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICVjCCAT4CAQAwETEPMA0GA1UEAwwGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEApv6CsVUEqYtMe+i615mbiRuHAqJcbZVZG9/3NCigOokW +TtHo9Ro5qQcVs4qcQl9Sv9GOfKLPFX3WbSqWwmKMD1ZHjK3TlHPEu8cSkuTh2W2h +AU2oYtDfSmRS5VaGbqrH1ZeT/E3tVOmqyqNvVoMa1EoolAbIg/UE+ZaTcGDgj/vd +AA0IrFOZ4wuhsbwvnS31j+mFMPVOVYDBRiOBpE98LNxRKP5dwUZJ8UMumGbCjw7w +ZbfDiRgbsOTh3AFl9qR+/tGKRKWFHeNDb1RKclXOg+qaE89HpxKP65pBI+MJpCLf +xvuRMAOj8tbtKv3CHn0fys7jkYPodCCFn3bSylgseQIDAQABoAAwDQYJKoZIhvcN +AQELBQADggEBAJqjxnD17EMw6Odz449LNHjU3bwLTFUALrIgEKASiAOedRBsM53v +68gd6YuYrKL74fwR+ud8ybotwXTEq0RNyNIDiwP8QKzdiIqp13hm74oG9BuXV4Sm +938XovDwX8AbpNKt5RA7mG1czE5ETeShMJrRZg4g0Fz9zzGptCD89WWHVpF8KQzd +4RPrbB11cxnOz1OPU+B20P1gf7n3RlRsFFMbTFkLdHqCjYeCoXcf2bMghoQWtKuG +RLqYMjlQKxPJLA8+k6VyGVJE7ETgzQ8+KrlYpqzqWDwNjbTKD1vJqXzGSQp4nuzR ++pSseB4snXeyv31tTA4SwUqw1vOtfVoU30s= +-----END CERTIFICATE REQUEST----- diff --git a/api-client/src/test/resources/certs/create-certs.sh b/api-client/src/test/resources/certs/create-certs.sh new file mode 100755 index 00000000..df47b890 --- /dev/null +++ b/api-client/src/test/resources/certs/create-certs.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# see https://docs.docker.com/articles/https/ + +echo 01 > ca.srl +openssl genrsa -des3 -out ca-key.pem 2048 +openssl req -new -x509 -days 365 -key ca-key.pem -out ca.pem + +openssl genrsa -des3 -out server-key.pem 2048 +openssl req -subj '/CN=localhost' -new -key server-key.pem -out server.csr + +openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca-key.pem -out server-cert.pem + +openssl genrsa -des3 -out key.pem 2048 +openssl req -subj '/CN=client' -new -key key.pem -out client.csr + +echo extendedKeyUsage = clientAuth > extfile.cnf +openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca-key.pem -out cert.pem -extfile extfile.cnf + +openssl rsa -in server-key.pem -out server-key.pem +openssl rsa -in key.pem -out key.pem diff --git a/api-client/src/test/resources/certs/extfile.cnf b/api-client/src/test/resources/certs/extfile.cnf new file mode 100644 index 00000000..74dedb38 --- /dev/null +++ b/api-client/src/test/resources/certs/extfile.cnf @@ -0,0 +1 @@ +extendedKeyUsage = clientAuth diff --git a/api-client/src/test/resources/certs/key.pem b/api-client/src/test/resources/certs/key.pem new file mode 100644 index 00000000..711a73b4 --- /dev/null +++ b/api-client/src/test/resources/certs/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEApv6CsVUEqYtMe+i615mbiRuHAqJcbZVZG9/3NCigOokWTtHo +9Ro5qQcVs4qcQl9Sv9GOfKLPFX3WbSqWwmKMD1ZHjK3TlHPEu8cSkuTh2W2hAU2o +YtDfSmRS5VaGbqrH1ZeT/E3tVOmqyqNvVoMa1EoolAbIg/UE+ZaTcGDgj/vdAA0I +rFOZ4wuhsbwvnS31j+mFMPVOVYDBRiOBpE98LNxRKP5dwUZJ8UMumGbCjw7wZbfD +iRgbsOTh3AFl9qR+/tGKRKWFHeNDb1RKclXOg+qaE89HpxKP65pBI+MJpCLfxvuR +MAOj8tbtKv3CHn0fys7jkYPodCCFn3bSylgseQIDAQABAoIBABTDB9SsUUwS+Yte +P/yWKHQ/a+Wz8fHd45pHmiBt/J3KdW+PQXtXq1uMefI8hXjrCQq4LxLn2v89Ce/A +n8XDIs61V2fyJKAMVKVT9sc0la/GpYHX+dtvRJOnLXc97rDLotS3Z7qypZMtCgLv +luQWoLtMDQd6YGunhtrREbujD8hWwJUBuOyJqJfM2rZlIttiiA399Wjvn3leDUE5 +EtBcG0vhUnCcZ4O5wzJL4j/+xRKhh3oVfvfO9lKdRTwHrbyddRR7WyTJvrTYOSKZ +0pItkRruJaNqvZE2PDkHKmknQFPFfVDkIQ5r4zuokUTHWfFfFQxkBwLSw4XJ3Rq5 +xtr8GhECgYEA0LKoXJ3PxE+y1+j0W/WyEWcYxC8z7wBoBH2nQzO4P3iuPta6FzZK +SC3v9MEAW4IkWZpe4rasWo9VCb6k07Rg2pdTgWr0wwkmgM83HGnH60QAYJOoOdMn +5aR+9KqM8BguHQo3RTRA0PBT+IwwkI5ByPLZk1qHzsuEcPFP4B+5YqcCgYEAzNgQ +6XDWXgNHHWG7WHnyK0ZpV5mp++RxogmDB3mMMEMtv1CrUztkWdv8fjZg1ttxv8bK +1RfbyiFSxVkneyAbHyC5PUCXIuUj4JKzEsMNOBCPF6UWz57DKHqG5OhhfV0FsVHF +njhushjjUbQFaRbMKp9e5i5iExsuhuZ9XNaDe98CgYBLIVX86Y8HRA3FLobxqBSv +sSa4Uanni4TmwPl69t6IcwB/fLvHaXL6Yxc0TFq9lq9RVoJ8KXkIrPrXJDugPxpy +9vFH8OImaF/lFTPo1afz6SodmmqATqTZRguyNsfAE2RebfQRxMrkrO/91TISiDPk +jJCJPJkReDC4nBa+P410lwKBgQC2AhlswiTG3dS3R5pF7/x79REuk8uUlBNfbWIu +JxKn41jnj47a4n+01CFSJ5D993fndYPNuWhB/j/kN+Y7oXhXvSS6RwTDLaQmK1LP +jd3V/eiWkspk6eTvt4baJ5xP3nJrboPPpMa+mhyJARJ5oJcCHB8fGQFtho5dsLic +38FuMwKBgQCHn/jSNPvA7UWB/wYjjCfwVk6Vc7s2mTatca+07s/P9yVHjOfZyvPF +dRVM23dpL6Nf1y2a1gyD+LtrdDOzQgISicIrqyNUPduTIWosGNFQKXEczplLCtXT +NFmSjagZ438+qxLnXKwPZ7LpcCFY/ZXSnK/Ufa2Uhm3zj6WwzdmjaA== +-----END RSA PRIVATE KEY----- diff --git a/api-client/src/test/resources/certs/server-cert.pem b/api-client/src/test/resources/certs/server-cert.pem new file mode 100644 index 00000000..ef103584 --- /dev/null +++ b/api-client/src/test/resources/certs/server-cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC5jCCAc4CAQIwDQYJKoZIhvcNAQELBQAwXjELMAkGA1UEBhMCREUxDzANBgNV +BAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMREwDwYDVQQKDAhnZXNlbGxpeDEa +MBgGA1UEAwwRVG9iaWFzIEdlc2VsbGNoZW4wHhcNMTUwMTAzMTQyNTMxWhcNMTYw +MTAzMTQyNTMxWjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQC6uDNNyNpDvMWVs+ho06l7HE/69QddLu6A9EA1ahyY +zr4KoGVYBSwE6ae8gvwFdnqN4ITBuTzf8rXH8CaOOM6qurbGcZEjG0G58KuS3MyE +vcCW47GF77pQYIwbmbJHhdyYsqO+D1alqUgIaH2Cmnr1yIrz5KJX7gziyGQ6Jd+V +ceLy+hjdgO7gAIvNIVTyBNnlRyTOOA9qub0SYpMalsMRjzDPPUnKaqPDYK+nuSA7 +TasbNcjXsbE5RvYHkq6HR4ND2uNqmG/zFQwuo12cYJ1ov6Kb8dyDDPwb/T8VBXy4 +CEA9ngGuzrU6vjL2p+BvqZyegCN30ARFzqaN7SeftXQDAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAGODttzfVhUQCtE4osLFGI/BtWdfd2tQv0lDa+gNZgFECxFncxu7 +a38GoNsnXzxa/4aP3mSDaFvCBa7KRYL5oBewacJpHIavSt8m4lJ6+kZWvBbUGcqE +CBJGsB86EIIVHzH8sanaCP+XwTFqQyvv9/vNokTGGwuHVQVT7T5G6gEOK0JSbn2S +S2uAinEZC/kb8BUAVZJ7A7/Fjkoc/SGUgalE05xjAdCZ09yENENJfgptwZ5629Kg +bYmj1WuoIpKuB1CDHLF9ciz46XbVcp22wTzPPD486gpGEJFyApR5ZjRDDlUeH5tI +iZ4xVvUZa4ThZE387bklKrMg1Zh6PiIcw3s= +-----END CERTIFICATE----- diff --git a/api-client/src/test/resources/certs/server-key.pem b/api-client/src/test/resources/certs/server-key.pem new file mode 100644 index 00000000..e9c8f76f --- /dev/null +++ b/api-client/src/test/resources/certs/server-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAurgzTcjaQ7zFlbPoaNOpexxP+vUHXS7ugPRANWocmM6+CqBl +WAUsBOmnvIL8BXZ6jeCEwbk83/K1x/AmjjjOqrq2xnGRIxtBufCrktzMhL3AluOx +he+6UGCMG5myR4XcmLKjvg9WpalICGh9gpp69ciK8+SiV+4M4shkOiXflXHi8voY +3YDu4ACLzSFU8gTZ5UckzjgParm9EmKTGpbDEY8wzz1Jymqjw2Cvp7kgO02rGzXI +17GxOUb2B5Kuh0eDQ9rjaphv8xUMLqNdnGCdaL+im/Hcgwz8G/0/FQV8uAhAPZ4B +rs61Or4y9qfgb6mcnoAjd9AERc6mje0nn7V0AwIDAQABAoIBAEOMcdr5B6UrvJs8 +255YvF5yTqjuTfyzBsuWEUiBm9jYKj4z6pXeQR+B3BHBnSToyMtc5woiVlh6OLMd +OQWInZN8OrUtzqDRsoRvAdL+l+qJrwunUcivgkqjsuwlPS7MyZJgRgZzDBigVhPc +oVNHSaZzJ3m2Q9jloLnGXyP+IZWxYIWOUXb753M4cKHi43Aj6c53Q1bZnEKCZZTO +0Z9DTPrY2C/KywcJOxvPDHqN+fmCViPWKIdLZvQTyJLA8TQ7uCEgNpEB9XfAyNSH +1aHiJEa8Hpmsib2R+skauXZq753Uin4V/EtJyjPcp9Ixbs2Kgdjh/twgnAW+isgr +XbfxVAkCgYEA2sukWlYakXeIxcKUJvdmgl3kvndxpmTzBCHmwj1X/e0v1dz5YcQN +S0UiT9H5gFWKWI+P5/R0G5/KGUTqn17AkbnNcNyZ/8epr7TBTtlIQRuOVVt8HROa +YIZsBcugwDkM3gOvL+2hI+JmPmSPmf3Zu2ndJYuW2P+dhSOCR3Waaw0CgYEA2nhD +u9DBM0ZIrRWfyohrYLwvDzC/GV4SZBQ1Y62baSfk9tiI6mm4o0FVL8Hw2xsyZqjX +Zy7jo0ZhrYPfrZQgO6JM2q8oje5NZEH8z/SgSibxeT5PSwlDKMiMSHz4IkFIaQLe +7peNaaKkr7oWGtrhR/6TKle61e0d1F4Ca8YhV08CgYBg4G5DOYDGDCFZPxCOOFkB +PEW+N/Dwdh5MJB6ivrqpSk416YPaUA5613+bzCMnObjqAZb8Y3bNjHDVw3gHXNLz +M0vNvz/Iy0VM8UHugbLGcUcScFGCJl3ig2JtJn5k3l3eJ89370W8N9CBq9ERpB62 +JueuaiI+rt+2RcS3k1DuOQKBgFIJy1Wh5Ir+n5fvgNurMrdWs00+/uPlwX43YH0X +2j44rvBLC88K+DzF8dlHOMzuf5SxXoHux6iua0ufM8WY1CnQno+YKuBxVjqJ1Qe6 +Ut7FlPP0hA4nYkLUXGl0FDG41hoo69Iosv+yBam/dUGMZACHBnhmDVD8A7GVw874 +zdhdAoGAF3m5HdmN0qsbiJBUuvugyOaRzfe+U/kzqio4HysHCPDOV5d4p6T4TjGR +eDj+zJebM5l2fCioo9JH9rs4Hprvjks4za2Qb9+XUoLhiaWLtPtp70srQbA1i+zf +E1ODVqFvDRhnTKG1Ys/x0WaGilmPRA78PDwcNYmDwDvnzRIDEec= +-----END RSA PRIVATE KEY----- diff --git a/api-client/src/test/resources/certs/server.csr b/api-client/src/test/resources/certs/server.csr new file mode 100644 index 00000000..437e97c6 --- /dev/null +++ b/api-client/src/test/resources/certs/server.csr @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAurgzTcjaQ7zFlbPoaNOpexxP+vUHXS7ugPRANWoc +mM6+CqBlWAUsBOmnvIL8BXZ6jeCEwbk83/K1x/AmjjjOqrq2xnGRIxtBufCrktzM +hL3AluOxhe+6UGCMG5myR4XcmLKjvg9WpalICGh9gpp69ciK8+SiV+4M4shkOiXf +lXHi8voY3YDu4ACLzSFU8gTZ5UckzjgParm9EmKTGpbDEY8wzz1Jymqjw2Cvp7kg +O02rGzXI17GxOUb2B5Kuh0eDQ9rjaphv8xUMLqNdnGCdaL+im/Hcgwz8G/0/FQV8 +uAhAPZ4Brs61Or4y9qfgb6mcnoAjd9AERc6mje0nn7V0AwIDAQABoAAwDQYJKoZI +hvcNAQELBQADggEBABgv+ZOgVByWPvQUYNhB6fpgq/w/kTyS0WGwJCWl8EZUvWWX +ePFdlwV5n3QYeRYJ2o0ZESra3TAhEoNCbju0Chy9tF7+SCyDH7ZrgIzON6mdTV8D +xpxc5VtNvWcVnBjekCNUG8XDdeCX5G6pa2b6nvpvVvUJO2+X01PXmR6tNwIpSJE3 +/3jYvoioN5o3Lmite6VJ443MVWXCjwxJ7x9xXl33yrsHbyZCsTJ9KogNG4XXyEDj +9Rx7RDvi9siRs/yEaCjtyA477N5crNqea3x7VNF0ZZv/1mI343blbsV5eMXUdeFG +oCfNkzQ8Y/rysRW9Li+GtrbQLxTDjW5e8HGuc04= +-----END CERTIFICATE REQUEST----- diff --git a/api-client/src/test/resources/context/config.json b/api-client/src/test/resources/context/config.json new file mode 100644 index 00000000..c21c32e1 --- /dev/null +++ b/api-client/src/test/resources/context/config.json @@ -0,0 +1,4 @@ +{ + "credsStore": "desktop", + "currentContext": "for-test" +} diff --git a/api-client/src/test/resources/context/contexts/meta/297dc204469307b573ca1e71dead5336f61c3aa222bf3a507cd59bf0c07a43b8/meta.json b/api-client/src/test/resources/context/contexts/meta/297dc204469307b573ca1e71dead5336f61c3aa222bf3a507cd59bf0c07a43b8/meta.json new file mode 100644 index 00000000..bf1ef630 --- /dev/null +++ b/api-client/src/test/resources/context/contexts/meta/297dc204469307b573ca1e71dead5336f61c3aa222bf3a507cd59bf0c07a43b8/meta.json @@ -0,0 +1,10 @@ +{ + "Name": "for-test", + "Metadata": {}, + "Endpoints": { + "docker": { + "Host": "unix:///var/run/docker.sock", + "SkipTLSVerify": false + } + } +} diff --git a/api-client/src/test/resources/context/contexts/meta/fe9c6bd7a66301f49ca9b6a70b217107cd1284598bfc254700c989b916da791e/meta.json b/api-client/src/test/resources/context/contexts/meta/fe9c6bd7a66301f49ca9b6a70b217107cd1284598bfc254700c989b916da791e/meta.json new file mode 100644 index 00000000..8a2598bf --- /dev/null +++ b/api-client/src/test/resources/context/contexts/meta/fe9c6bd7a66301f49ca9b6a70b217107cd1284598bfc254700c989b916da791e/meta.json @@ -0,0 +1,11 @@ +{ + "Name": "desktop-linux", + "Metadata": { + }, + "Endpoints": { + "docker": { + "Host": "npipe:////./pipe/dockerDesktopLinuxEngine", + "SkipTLSVerify": false + } + } +} diff --git a/api-client/src/test/resources/context/docker-default-context.json b/api-client/src/test/resources/context/docker-default-context.json new file mode 100644 index 00000000..964148f1 --- /dev/null +++ b/api-client/src/test/resources/context/docker-default-context.json @@ -0,0 +1,17 @@ +{ + "Name": "default", + "Metadata": { + "StackOrchestrator": "swarm" + }, + "Endpoints": { + "docker": { + "Host": "npipe:////./pipe/docker_engine", + "SkipTLSVerify": false + } + }, + "TLSMaterial": {}, + "Storage": { + "MetadataPath": "\u003cIN MEMORY\u003e", + "TLSPath": "\u003cIN MEMORY\u003e" + } +} diff --git a/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/ECDSA/keys/ecdsa.pem b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/ECDSA/keys/ecdsa.pem new file mode 100644 index 00000000..97f62754 --- /dev/null +++ b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/ECDSA/keys/ecdsa.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg9eWdXw19hL94+Jx1xjb79Y3Hr9rAaRYaoe4XSv6BnPigCgYIKoZIzj0DAQehRANCAAR9VOiSABvXFHeq/hCMEx63Vq0mYneI2aqQu5sLu5x8DrzUd82BodKoUG3dMPWY9m86dGYAR9xhVUlBDpap9TfH +-----END PRIVATE KEY----- diff --git a/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/ECDSA/keys/ecdsa_with_params.pem b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/ECDSA/keys/ecdsa_with_params.pem new file mode 100644 index 00000000..04b25791 --- /dev/null +++ b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/ECDSA/keys/ecdsa_with_params.pem @@ -0,0 +1,29 @@ +-----BEGIN EC PARAMETERS----- +MIIBwgIBATBNBgcqhkjOPQEBAkIB//////////////////////////////////// +//////////////////////////////////////////////////8wgZ4EQgH///// +//////////////////////////////////////////////////////////////// +/////////////////ARBUZU+uWGOHJofkpohoLaFQO6i2nJbmbMV87i0iZGO8Qnh +Vhk5Uex+k3sWUsC9O7G/BzVz34g9LDTx70Uf1GtQPwADFQDQnogAKRy4U5bMZxc5 +MoSqoNpkugSBhQQAxoWOBrcEBOnNnj7LZiOVtEKcZIE5BT+1Ifgor2BrTT26oUte +d+/nWSj+HcEnov+o3jNIs8GFakKb+X5+McLlvWYBGDkpaniaO8AEXIpftCx9G9mY +9URJV5tEaBevvRcnPmYsl+5ymV70JkDFULkBP60HYTU8cIaicsJAiL6Udp/RZlAC +QgH///////////////////////////////////////////pRhoeDvy+Wa3/MAUj3 +CaXQO7XJuImcR667b7cekThkCQIBAQ== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIICnQIBAQRCAYv7H1FkaDUX5lkQ7u6tlJiu/Bl6KQYI5zO8vuX/lQp5smi0DPAj +4YGtyRHV17ESgvlvWxZ6/5FzJN9FEGtWUuN7oIIBxjCCAcICAQEwTQYHKoZIzj0B +AQJCAf////////////////////////////////////////////////////////// +////////////////////////////MIGeBEIB//////////////////////////// +//////////////////////////////////////////////////////////wEQVGV +PrlhjhyaH5KaIaC2hUDuotpyW5mzFfO4tImRjvEJ4VYZOVHsfpN7FlLAvTuxvwc1 +c9+IPSw08e9FH9RrUD8AAxUA0J6IACkcuFOWzGcXOTKEqqDaZLoEgYUEAMaFjga3 +BATpzZ4+y2YjlbRCnGSBOQU/tSH4KK9ga009uqFLXnfv51ko/h3BJ6L/qN4zSLPB +hWpCm/l+fjHC5b1mARg5KWp4mjvABFyKX7QsfRvZmPVESVebRGgXr70XJz5mLJfu +cple9CZAxVC5AT+tB2E1PHCGonLCQIi+lHaf0WZQAkIB//////////////////// +///////////////////////6UYaHg78vlmt/zAFI9wml0Du1ybiJnEeuu2+3HpE4 +ZAkCAQGhgYkDgYYABACqJZpQyXmp3I3/uZiVJVp/kod3HibAoA9JnLCgVP/KSCBf +lGrmkEQPb9Toxq65Mn7fa0TxWGOwk+mNQoc3vxQ9vgGH7IV1Y0tw42Zkm92B+amW +svS8FYGy1iac735UYPQAHs+W2cAMJj5HxWV1TFRsvpA786E4qZIOfSig8pqOPUco +Ug== +-----END EC PRIVATE KEY----- diff --git a/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/certpath/ca.pem b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/certpath/ca.pem new file mode 100644 index 00000000..4f67063e --- /dev/null +++ b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/certpath/ca.pem @@ -0,0 +1,48 @@ +-----BEGIN CERTIFICATE----- +MIID7TCCAtWgAwIBAgIJAL2CQAue4q4FMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV +BAYTAkNSMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxEDAOBgNVBAMTB2NhLXRlc3QwHhcNMTcwMzA5MTQwNzUw +WhcNMTcwNDA4MTQwNzUwWjBXMQswCQYDVQQGEwJDUjETMBEGA1UECBMKU29tZS1T +dGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQD +EwdjYS10ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2wDXfSG6 +HvtguxTuUvbJSKAtH6E/rINjmtJqrtWVBXOjzuQxQuXiiqRnpvr+l4SgpG2RZ1WI +l2zKAmcBThXqoF04v3NGX8j2M+532+tcRsG0I5lAcCrCih0jzUPAJfSxXpyYYLdh +E9f9YPQURD1V9ReeqGj/LI9Dyk8T9GSFP3x2k3kk2V4Py0GIIoumGIYPhS98FcqC +g1pPPjuVnSNJLMMDwkp82VI3zNb90NDIbNALWgoowAxn3KlTsUH/mHXyBHk3AZe+ +HszKShhBg+mxPBgQaF3QLHqPt8fR5nY0rS7bxWKTkCPSptJo1snEw61L9aqkYYih +fp9UtY1H74HDhQIDAQABo4G7MIG4MB0GA1UdDgQWBBRVn/zXNPQ59p1OB3m+va0T +R+aSnTCBiAYDVR0jBIGAMH6AFFWf/Nc09Dn2nU4Heb69rRNH5pKdoVukWTBXMQsw +CQYDVQQGEwJDUjETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJu +ZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQDEwdjYS10ZXN0ggkAvYJAC57irgUw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAmhDR/NgPif0m48rtCZiN +SNHA5a9GkjXUuWnqVGIEpcYa6VR0mkUG7Tu6gtGY/Ev+Jcjw/8+pMNXMrdLt/Bmz +uz4ZiWKKcB42PYyqCZzut/MtQcs5pBm8cYyQzPFwngb3AJ+/aD942TK+kxZwHSM2 +DGJ9j5W1vr7agJemileSKkgSm+iHdg7Wf5fg9PQpRnZcJYB1rM77erFg24yX/I4p +GE2kRNB69tVEQfxi/Pb8bX2SALC9J/UyvDSZhTZDZVK9jX7lXSmam+uIeY7yVPsC +QruOo8I5Bhcgrku/WBViNWdHCOvFk1uGhdFElNun6n8/4yshnV4GCzwgYTlBZoHr +dQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID9DCCAtygAwIBAgIJAIjDwFs6JTALMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAkNSMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWNhLXRlc3QtMjAeFw0xNzAzMDkxNDMw +MjlaFw0xNzA0MDgxNDMwMjlaMFkxCzAJBgNVBAYTAkNSMRMwEQYDVQQIEwpTb21l +LVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV +BAMTCWNhLXRlc3QtMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPbi +nFVvaRRrDoD5tj/nv8i6T3xptwPLTP83gdFSuuUCuNybaU1q+RWMXpX/Bu1m9y/Q +G1VJOSVZRna84EOr2dFazHxo/YbGFmnAnPzKyEuJ15mQWGaN0hlB3QH2v6mYDIVN +IUWCa3gP+7cBeFRp+ow0HOvFYHWQ0rCaAop/Nr100jyl4JzI3vr/Ur9fcV9cBFKz +0L+mp65psZ9h63KCyxpinHZ/TZ87sqqSLbIJTzhYgqYUWSiDoX3CyQdyNPf7lM+0 +2yoFT/RGSJ0fzY4hv77itQWKTTSMLKWbx6V56OadxO5zQVj3vw/gHstMTvurbWbC +aaTB/4VCfDrQqt6QCe0CAwEAAaOBvjCBuzAdBgNVHQ4EFgQUdFO4RZDq+vcXE1Gu +B7vLKf9jP+wwgYsGA1UdIwSBgzCBgIAUdFO4RZDq+vcXE1GuB7vLKf9jP+yhXaRb +MFkxCzAJBgNVBAYTAkNSMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJ +bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWNhLXRlc3QtMoIJAIjD +wFs6JTALMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGJ/YQQVQCqZ +OONpYd6nisL8AtbDXQ5r/j6JWup297YcvzseQOfglGUcD0QZ+rcI4F55RcAvqRM3 +ifUXVh6h1OBxV6H6GSqcxanqy6T9G2iMjkz7DMSS9UJrFmGgYCysnB/YlTvlgcR6 +hIbL4mLwLG1PFUrcHiOJkgDMUgveTfGN4AiBqBWTAKCiaCVJRlbAS1hVD1xoJARa +PsoSJ9XXoe7PoQrLdXcrp0xw8GtCJuW2pDuY2RCprZ2tS+CMD5rs0eL4tGgBc7IY +iqd7oqLXUSXlr9vZAp6FkLlNanz045m5MeTetSo9Fcm+nn3ydDInmxBsAiyOZTI6 +HUvV0zFTq+U= +-----END CERTIFICATE----- diff --git a/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/certpath/cert.pem b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/certpath/cert.pem new file mode 100644 index 00000000..5431b60a --- /dev/null +++ b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/certpath/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDAjCCAeqgAwIBAgIJAP9/tvpflc6RMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV +BAYTAkNSMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxEDAOBgNVBAMTB2NhLXRlc3QwHhcNMTcwMzA5MTQyMTU2 +WhcNMTcwNDA4MTQyMTU2WjARMQ8wDQYDVQQDEwZjbGllbnQwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCYJt9YNe8zTA6Bctiemw85snVZqkAVe6Xrc/he +RRHH89+jxHgS8arLpfffdyMGGYOC6R/4tSS8hCu/3J0S8CZgOfRwnzOWOyt0UbKt +t6m8RmV7fNnuNVCXdHMZB+W7gK75YDqbYnHHvacDu9o3CA2tguTgWYM2XcDfTu72 +IYhvT3TwMuWPK8oKDs5IKeez/F+dYQ9GA1WYWPIsOVAbclfxWcrTg80NqvzVfttG +rqExBOXHnCwuiPTg3X41CFwOSkTya0PZ94dq3819Q77FEsXZzrN4fgQA+WX9KTCk +d5oONI0R870Cz3PjS5/aQm5CLPle/AxwjwQLY3f8rUxrwnI5AgMBAAGjFzAVMBMG +A1UdJQQMMAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBCwUAA4IBAQDILcvBMHz8R8NO +D3vuHtobRH2bI2PTnxY9immRAoiLB26VdxPGU/aPHKJwj3BvJEC6W4ZX9nNW5Ua7 +WTB+A6TlQ9XN3qL1izK/+C8eA0KDj+UVzq8ZlLzgjz80ZPtzMVBRGFBCWmuTFH0k +64fH4I+v2tXxSqVbIQ7R/LauaRrRIzMZNwERdQ4RFDhxIRFCCYu3HWw+Wa9OrFxW +bm9zFwHfBF0jnhV9RcFqLkUtDM+1oN+BKrwFqASQHEm4W4SAec8K7/IaijFRyYLl +N2dKfmPsBnuhiMiNWbHKUESWoVy4J7rkFlUKxKczlOZYQjQuyI8JlpKu80AxZPlg +BksOkH+8 +-----END CERTIFICATE----- diff --git a/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/certpath/key.pem b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/certpath/key.pem new file mode 100644 index 00000000..869198ef --- /dev/null +++ b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/certpath/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAmCbfWDXvM0wOgXLYnpsPObJ1WapAFXul63P4XkURx/Pfo8R4 +EvGqy6X333cjBhmDgukf+LUkvIQrv9ydEvAmYDn0cJ8zljsrdFGyrbepvEZle3zZ +7jVQl3RzGQflu4Cu+WA6m2Jxx72nA7vaNwgNrYLk4FmDNl3A307u9iGIb0908DLl +jyvKCg7OSCnns/xfnWEPRgNVmFjyLDlQG3JX8VnK04PNDar81X7bRq6hMQTlx5ws +Loj04N1+NQhcDkpE8mtD2feHat/NfUO+xRLF2c6zeH4EAPll/SkwpHeaDjSNEfO9 +As9z40uf2kJuQiz5XvwMcI8EC2N3/K1Ma8JyOQIDAQABAoIBAQCRCKTnienshPhi +zp3MelRGFGWmRhJ2J/pd+ZrsCXzMlBw/GH242SKMozUnj4IUQ2fH9v7h7ZIKHRcy ++VHFBqQeH8BxRmA/OUHPzDy9ORCUBqqfzsBSQAIFjRxV5OBJHn4r+l2DVpDM7Gxy +s7Nrt8KJ/fa8ILdhsHa0pqJEfmx2ZNfpLT0ih6Nbp59HtwucQxvodOJkKIwzv9GC +QCD/N8UGnYoyfUB4dgxqMyN16mY5/wZlCLJj7e7JRQ08gCAjQ/Ox4anWioO3qEf+ +p9EaV2yPhnyX13A21BIphL/Ue0IkEOSYF7ukgjGwRozUk2VPTz38eEgDUHJ41/Pp +xtHF8zr9AoGBAMkTjcEfV/gdYZNeqHrMw1vRI2a3L+Fu0PfwRHjsHXtlrtUcWjTL +31/6FF+JlwXN4qptGdtn1jqTXEuL/n9pc3F9ngC/y0uxqt7MIuFyJphgNLXCBnIJ +LSZu2yVo7rY35TxHBxD+243EPtDfGn61LrDi5XdoMW4kk1WoIsTlTCjzAoGBAMG2 +OgWb+I/wXFu6C+NX9hMt1EHGMFsWASv+pz3fbnsHxC7ktGhAU4D1It6sjskhaAzf ++jGc98hbk9l2jyCLZiZ310pz+hLsjr1Ect5fUQoo1OoP3eP1KnNrrIHcnE6xVImn +vAbeeEsIxubogF1lbg1cg0Xd8kES6KcgwMEyKgMjAoGBAJoZpD/nd5GtYsSAfPpv +xyCs7UahCc7pELmTp8ZMnmOdkBm/OtrPjAeQLuDxH47RNO+L4Y8myyXlHYzAY3De +Lh1COj80vebq6JsLM4g35aBNytepNStcIQAdYVIQUzHPMJ27iYza5QSG6+VDtph4 +3qmtmlKixY819LxmuQ7Q9EdFAoGAA+8igIipZKr6BTbjFOvtaHhOzkMrKj5pKa6T +UPNfuAnxVw54B4H+8CiKW8FfaQBPr6tA1o8cjSom94yr3XpXScK3UaDHXGkHgRqJ +xrBi4fTwVgyacg3BnfR63WxQlFyPnfgVHoKYKNX1zCy+pqbvtD/DvsCCMgYjSXml +0IanSccCgYEAi4yucdsXNznykCQvXTuRAVS35ZOQqlvomsbihcYDck/wxBNuUJ92 +bpn/IR9ugJngUemERky2XF133/2jO5bXVIc2QAb8aoYCYH8l6J0DvTdyuDyejvGy +I+CmnrWLC0JviALefRIsappdiwhUJ71R40Hc9YI8Ba0MeKBL5RU7a3E= +-----END RSA PRIVATE KEY----- diff --git a/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/keys/invalid.pem b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/keys/invalid.pem new file mode 100644 index 00000000..b649a9bf --- /dev/null +++ b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/keys/invalid.pem @@ -0,0 +1 @@ +some text \ No newline at end of file diff --git a/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/keys/pkcs1.pem b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/keys/pkcs1.pem new file mode 100644 index 00000000..869198ef --- /dev/null +++ b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/keys/pkcs1.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAmCbfWDXvM0wOgXLYnpsPObJ1WapAFXul63P4XkURx/Pfo8R4 +EvGqy6X333cjBhmDgukf+LUkvIQrv9ydEvAmYDn0cJ8zljsrdFGyrbepvEZle3zZ +7jVQl3RzGQflu4Cu+WA6m2Jxx72nA7vaNwgNrYLk4FmDNl3A307u9iGIb0908DLl +jyvKCg7OSCnns/xfnWEPRgNVmFjyLDlQG3JX8VnK04PNDar81X7bRq6hMQTlx5ws +Loj04N1+NQhcDkpE8mtD2feHat/NfUO+xRLF2c6zeH4EAPll/SkwpHeaDjSNEfO9 +As9z40uf2kJuQiz5XvwMcI8EC2N3/K1Ma8JyOQIDAQABAoIBAQCRCKTnienshPhi +zp3MelRGFGWmRhJ2J/pd+ZrsCXzMlBw/GH242SKMozUnj4IUQ2fH9v7h7ZIKHRcy ++VHFBqQeH8BxRmA/OUHPzDy9ORCUBqqfzsBSQAIFjRxV5OBJHn4r+l2DVpDM7Gxy +s7Nrt8KJ/fa8ILdhsHa0pqJEfmx2ZNfpLT0ih6Nbp59HtwucQxvodOJkKIwzv9GC +QCD/N8UGnYoyfUB4dgxqMyN16mY5/wZlCLJj7e7JRQ08gCAjQ/Ox4anWioO3qEf+ +p9EaV2yPhnyX13A21BIphL/Ue0IkEOSYF7ukgjGwRozUk2VPTz38eEgDUHJ41/Pp +xtHF8zr9AoGBAMkTjcEfV/gdYZNeqHrMw1vRI2a3L+Fu0PfwRHjsHXtlrtUcWjTL +31/6FF+JlwXN4qptGdtn1jqTXEuL/n9pc3F9ngC/y0uxqt7MIuFyJphgNLXCBnIJ +LSZu2yVo7rY35TxHBxD+243EPtDfGn61LrDi5XdoMW4kk1WoIsTlTCjzAoGBAMG2 +OgWb+I/wXFu6C+NX9hMt1EHGMFsWASv+pz3fbnsHxC7ktGhAU4D1It6sjskhaAzf ++jGc98hbk9l2jyCLZiZ310pz+hLsjr1Ect5fUQoo1OoP3eP1KnNrrIHcnE6xVImn +vAbeeEsIxubogF1lbg1cg0Xd8kES6KcgwMEyKgMjAoGBAJoZpD/nd5GtYsSAfPpv +xyCs7UahCc7pELmTp8ZMnmOdkBm/OtrPjAeQLuDxH47RNO+L4Y8myyXlHYzAY3De +Lh1COj80vebq6JsLM4g35aBNytepNStcIQAdYVIQUzHPMJ27iYza5QSG6+VDtph4 +3qmtmlKixY819LxmuQ7Q9EdFAoGAA+8igIipZKr6BTbjFOvtaHhOzkMrKj5pKa6T +UPNfuAnxVw54B4H+8CiKW8FfaQBPr6tA1o8cjSom94yr3XpXScK3UaDHXGkHgRqJ +xrBi4fTwVgyacg3BnfR63WxQlFyPnfgVHoKYKNX1zCy+pqbvtD/DvsCCMgYjSXml +0IanSccCgYEAi4yucdsXNznykCQvXTuRAVS35ZOQqlvomsbihcYDck/wxBNuUJ92 +bpn/IR9ugJngUemERky2XF133/2jO5bXVIc2QAb8aoYCYH8l6J0DvTdyuDyejvGy +I+CmnrWLC0JviALefRIsappdiwhUJ71R40Hc9YI8Ba0MeKBL5RU7a3E= +-----END RSA PRIVATE KEY----- diff --git a/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/keys/pkcs8.pem b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/keys/pkcs8.pem new file mode 100644 index 00000000..1268bb6a --- /dev/null +++ b/api-client/src/test/resources/de/gesellix/docker/ssl/algorithm/RSA/keys/pkcs8.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCYJt9YNe8zTA6B +ctiemw85snVZqkAVe6Xrc/heRRHH89+jxHgS8arLpfffdyMGGYOC6R/4tSS8hCu/ +3J0S8CZgOfRwnzOWOyt0UbKtt6m8RmV7fNnuNVCXdHMZB+W7gK75YDqbYnHHvacD +u9o3CA2tguTgWYM2XcDfTu72IYhvT3TwMuWPK8oKDs5IKeez/F+dYQ9GA1WYWPIs +OVAbclfxWcrTg80NqvzVfttGrqExBOXHnCwuiPTg3X41CFwOSkTya0PZ94dq3819 +Q77FEsXZzrN4fgQA+WX9KTCkd5oONI0R870Cz3PjS5/aQm5CLPle/AxwjwQLY3f8 +rUxrwnI5AgMBAAECggEBAJEIpOeJ6eyE+GLOncx6VEYUZaZGEnYn+l35muwJfMyU +HD8YfbjZIoyjNSePghRDZ8f2/uHtkgodFzL5UcUGpB4fwHFGYD85Qc/MPL05EJQG +qp/OwFJAAgWNHFXk4Ekefiv6XYNWkMzsbHKzs2u3won99rwgt2GwdrSmokR+bHZk +1+ktPSKHo1unn0e3C5xDG+h04mQojDO/0YJAIP83xQadijJ9QHh2DGozI3XqZjn/ +BmUIsmPt7slFDTyAICND87HhqdaKg7eoR/6n0RpXbI+GfJfXcDbUEimEv9R7QiQQ +5JgXu6SCMbBGjNSTZU9PPfx4SANQcnjX8+nG0cXzOv0CgYEAyRONwR9X+B1hk16o +eszDW9EjZrcv4W7Q9/BEeOwde2Wu1RxaNMvfX/oUX4mXBc3iqm0Z22fWOpNcS4v+ +f2lzcX2eAL/LS7Gq3swi4XImmGA0tcIGcgktJm7bJWjutjflPEcHEP7bjcQ+0N8a +frUusOLld2gxbiSTVagixOVMKPMCgYEAwbY6BZv4j/BcW7oL41f2Ey3UQcYwWxYB +K/6nPd9uewfELuS0aEBTgPUi3qyOySFoDN/6MZz3yFuT2XaPIItmJnfXSnP6EuyO +vURy3l9RCijU6g/d4/Uqc2usgdycTrFUiae8Bt54SwjG5uiAXWVuDVyDRd3yQRLo +pyDAwTIqAyMCgYEAmhmkP+d3ka1ixIB8+m/HIKztRqEJzukQuZOnxkyeY52QGb86 +2s+MB5Au4PEfjtE074vhjybLJeUdjMBjcN4uHUI6PzS95uromwsziDfloE3K16k1 +K1whAB1hUhBTMc8wnbuJjNrlBIbr5UO2mHjeqa2aUqLFjzX0vGa5DtD0R0UCgYAD +7yKAiKlkqvoFNuMU6+1oeE7OQysqPmkprpNQ81+4CfFXDngHgf7wKIpbwV9pAE+v +q0DWjxyNKib3jKvdeldJwrdRoMdcaQeBGonGsGLh9PBWDJpyDcGd9HrdbFCUXI+d ++BUegpgo1fXMLL6mpu+0P8O+wIIyBiNJeaXQhqdJxwKBgQCLjK5x2xc3OfKQJC9d +O5EBVLflk5CqW+iaxuKFxgNyT/DEE25Qn3Zumf8hH26AmeBR6YRGTLZcXXff/aM7 +ltdUhzZABvxqhgJgfyXonQO9N3K4PJ6O8bIj4KaetYsLQm+IAt59Eixqml2LCFQn +vVHjQdz1gjwFrQx4oEvlFTtrcQ== +-----END PRIVATE KEY----- diff --git a/api-client/src/test/resources/docker/.dockerignore b/api-client/src/test/resources/docker/.dockerignore new file mode 100644 index 00000000..514d05a2 --- /dev/null +++ b/api-client/src/test/resources/docker/.dockerignore @@ -0,0 +1,3 @@ +i-should-be-ignored.txt +**/ignore-me.txt +subdirectory/i-should-be-ignored-too.txt diff --git a/api-client/src/test/resources/docker/Dockerfile b/api-client/src/test/resources/docker/Dockerfile new file mode 100644 index 00000000..2df7d103 --- /dev/null +++ b/api-client/src/test/resources/docker/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine:edge +MAINTAINER Tobias Gesellchen + +ADD ./subdirectory/payload.txt /payload.txt diff --git a/api-client/src/test/resources/docker/i-should-be-ignored.txt b/api-client/src/test/resources/docker/i-should-be-ignored.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/docker/script.sh b/api-client/src/test/resources/docker/script.sh new file mode 100755 index 00000000..aa6cfe19 --- /dev/null +++ b/api-client/src/test/resources/docker/script.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "hello world" diff --git a/api-client/src/test/resources/docker/subdirectory/i-should-be-ignored-too.txt b/api-client/src/test/resources/docker/subdirectory/i-should-be-ignored-too.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/docker/subdirectory/ignore-me.txt b/api-client/src/test/resources/docker/subdirectory/ignore-me.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/docker/subdirectory/payload.txt b/api-client/src/test/resources/docker/subdirectory/payload.txt new file mode 100644 index 00000000..f0eec86f --- /dev/null +++ b/api-client/src/test/resources/docker/subdirectory/payload.txt @@ -0,0 +1 @@ +some content \ No newline at end of file diff --git a/api-client/src/test/resources/dockerignore-all-but-some/.dockerignore b/api-client/src/test/resources/dockerignore-all-but-some/.dockerignore new file mode 100644 index 00000000..73632db7 --- /dev/null +++ b/api-client/src/test/resources/dockerignore-all-but-some/.dockerignore @@ -0,0 +1,3 @@ +* +!Dockerfile +!content.txt diff --git a/api-client/src/test/resources/dockerignore-all-but-some/.hidden-dir/.gitkeep b/api-client/src/test/resources/dockerignore-all-but-some/.hidden-dir/.gitkeep new file mode 100644 index 00000000..3857d287 --- /dev/null +++ b/api-client/src/test/resources/dockerignore-all-but-some/.hidden-dir/.gitkeep @@ -0,0 +1 @@ +this one is hidden, similar to a .git folder with its contents diff --git a/api-client/src/test/resources/dockerignore-all-but-some/Dockerfile b/api-client/src/test/resources/dockerignore-all-but-some/Dockerfile new file mode 100644 index 00000000..9eb15be5 --- /dev/null +++ b/api-client/src/test/resources/dockerignore-all-but-some/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:edge +COPY . /tmp +CMD [ "ls", "-h", "/tmp" ] diff --git a/api-client/src/test/resources/dockerignore-all-but-some/content.txt b/api-client/src/test/resources/dockerignore-all-but-some/content.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/dockerignore-all-but-some/ignore-me.txt b/api-client/src/test/resources/dockerignore-all-but-some/ignore-me.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/dockerignore/.dockerignore b/api-client/src/test/resources/dockerignore/.dockerignore new file mode 100644 index 00000000..7bb1daa5 --- /dev/null +++ b/api-client/src/test/resources/dockerignore/.dockerignore @@ -0,0 +1,3 @@ +ignorefolder +!ignorefolder/keepme.txt +**/ignore.txt diff --git a/api-client/src/test/resources/dockerignore/ignorefolder/content.txt b/api-client/src/test/resources/dockerignore/ignorefolder/content.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/dockerignore/ignorefolder/keepme.txt b/api-client/src/test/resources/dockerignore/ignorefolder/keepme.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/dockerignore/subfolder/content.txt b/api-client/src/test/resources/dockerignore/subfolder/content.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/dockerignore/subfolder/ignore.txt b/api-client/src/test/resources/dockerignore/subfolder/ignore.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/dockerignore/subfolder/subsubfolder/content.txt b/api-client/src/test/resources/dockerignore/subfolder/subsubfolder/content.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/dockerignore_subdirs/.dockerignore b/api-client/src/test/resources/dockerignore_subdirs/.dockerignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/api-client/src/test/resources/dockerignore_subdirs/.dockerignore @@ -0,0 +1 @@ +node_modules/ diff --git a/api-client/src/test/resources/dockerignore_subdirs/keepme/a-file-to-be-kept.txt b/api-client/src/test/resources/dockerignore_subdirs/keepme/a-file-to-be-kept.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/dockerignore_subdirs/keepme/subdir/keep-me.txt b/api-client/src/test/resources/dockerignore_subdirs/keepme/subdir/keep-me.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/dockerignore_subdirs/node_modules/a-file-to-be-excluded.txt b/api-client/src/test/resources/dockerignore_subdirs/node_modules/a-file-to-be-excluded.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/dockerignore_subdirs/node_modules/ignored_subdir/another-file-to-be-excluded.txt b/api-client/src/test/resources/dockerignore_subdirs/node_modules/ignored_subdir/another-file-to-be-excluded.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/dockerignore_windows/.dockerignore b/api-client/src/test/resources/dockerignore_windows/.dockerignore new file mode 100644 index 00000000..49445af8 --- /dev/null +++ b/api-client/src/test/resources/dockerignore_windows/.dockerignore @@ -0,0 +1,2 @@ +bin\ +Dockerfile diff --git a/api-client/src/test/resources/dockerignore_windows/Dockerfile b/api-client/src/test/resources/dockerignore_windows/Dockerfile new file mode 100644 index 00000000..7a00da37 --- /dev/null +++ b/api-client/src/test/resources/dockerignore_windows/Dockerfile @@ -0,0 +1,3 @@ +FROM microsoft/aspnetcore:2.0 +COPY . /example +RUN ls -lisah /example diff --git a/api-client/src/test/resources/dockerignore_windows/bin/to-be-excluded.txt b/api-client/src/test/resources/dockerignore_windows/bin/to-be-excluded.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/dockerignore_windows/keepme/to-be-kept.txt b/api-client/src/test/resources/dockerignore_windows/keepme/to-be-kept.txt new file mode 100644 index 00000000..e69de29b diff --git a/api-client/src/test/resources/logback-test.xml b/api-client/src/test/resources/logback-test.xml index 268237eb..6ede53ca 100644 --- a/api-client/src/test/resources/logback-test.xml +++ b/api-client/src/test/resources/logback-test.xml @@ -8,6 +8,9 @@ + + + diff --git a/build.gradle.kts b/build.gradle.kts index b2fb7cae..87886b55 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,7 @@ plugins { val dependencyVersions = listOf( "commons-io:commons-io:2.21.0", + "net.bytebuddy:byte-buddy:1.17.8", "org.apache.commons:commons-compress:1.28.0", "org.apiguardian:apiguardian-api:1.1.2", "org.jetbrains:annotations:26.0.2-1",