diff --git a/engine/src/main/java/de/gesellix/docker/authentication/AuthConfigReader.java b/engine/src/main/java/de/gesellix/docker/authentication/AuthConfigReader.java index 9129726c..83c6df4f 100644 --- a/engine/src/main/java/de/gesellix/docker/authentication/AuthConfigReader.java +++ b/engine/src/main/java/de/gesellix/docker/authentication/AuthConfigReader.java @@ -1,14 +1,11 @@ package de.gesellix.docker.authentication; -import com.squareup.moshi.Moshi; +import de.gesellix.docker.engine.DockerConfigReader; import de.gesellix.docker.engine.DockerEnv; -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; import static de.gesellix.docker.authentication.AuthConfig.EMPTY_AUTH_CONFIG; @@ -17,9 +14,8 @@ public class AuthConfigReader { private final static Logger log = LoggerFactory.getLogger(AuthConfigReader.class); - private final Moshi moshi = new Moshi.Builder().build(); - private final DockerEnv env; + private DockerConfigReader dockerConfigReader; public AuthConfigReader() { this(new DockerEnv()); @@ -27,11 +23,12 @@ public AuthConfigReader() { public AuthConfigReader(DockerEnv env) { this.env = env; + this.dockerConfigReader = env.getDockerConfigReader(); } // @Override public AuthConfig readDefaultAuthConfig() { - return readAuthConfig(null, env.getDockerConfigFile()); + return readAuthConfig(null, dockerConfigReader.getDockerConfigFile()); } // @Override @@ -42,7 +39,7 @@ public AuthConfig readAuthConfig(String hostname, File dockerCfg) { hostname = env.getIndexUrl_v1(); } - Map parsedDockerCfg = readDockerConfigFile(dockerCfg); + Map parsedDockerCfg = dockerConfigReader.readDockerConfigFile(dockerCfg); if (parsedDockerCfg == null || parsedDockerCfg.isEmpty()) { return EMPTY_AUTH_CONFIG; } @@ -51,24 +48,6 @@ public AuthConfig readAuthConfig(String hostname, File dockerCfg) { return credsStore.getAuthConfig(hostname); } - public Map readDockerConfigFile(File dockerCfg) { - if (dockerCfg == null) { - dockerCfg = env.getDockerConfigFile(); - } - if (dockerCfg == null || !dockerCfg.exists()) { - log.info("docker config '${dockerCfg}' doesn't exist"); - return Collections.emptyMap(); - } - log.debug("reading auth info from {}", dockerCfg); - try { - return moshi.adapter(Map.class).fromJson(Okio.buffer(Okio.source(dockerCfg))); - } - catch (Exception e) { - log.debug(MessageFormat.format("failed to read auth info from {}", dockerCfg), e); - return Collections.emptyMap(); - } - } - public CredsStore getCredentialsStore(Map parsedDockerCfg) { return getCredentialsStore(parsedDockerCfg, ""); } diff --git a/engine/src/main/java/de/gesellix/docker/context/ContextStore.java b/engine/src/main/java/de/gesellix/docker/context/ContextStore.java new file mode 100644 index 00000000..babc6d18 --- /dev/null +++ b/engine/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/engine/src/main/java/de/gesellix/docker/context/DockerContext.java b/engine/src/main/java/de/gesellix/docker/context/DockerContext.java new file mode 100644 index 00000000..8b271470 --- /dev/null +++ b/engine/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/engine/src/main/java/de/gesellix/docker/context/DockerContextResolver.java b/engine/src/main/java/de/gesellix/docker/context/DockerContextResolver.java new file mode 100644 index 00000000..a795ccc0 --- /dev/null +++ b/engine/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/engine/src/main/java/de/gesellix/docker/context/EndpointMetaBase.java b/engine/src/main/java/de/gesellix/docker/context/EndpointMetaBase.java new file mode 100644 index 00000000..e1792424 --- /dev/null +++ b/engine/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/engine/src/main/java/de/gesellix/docker/context/Metadata.java b/engine/src/main/java/de/gesellix/docker/context/Metadata.java new file mode 100644 index 00000000..5d9f0856 --- /dev/null +++ b/engine/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/engine/src/main/java/de/gesellix/docker/context/MetadataStore.java b/engine/src/main/java/de/gesellix/docker/context/MetadataStore.java new file mode 100644 index 00000000..65166183 --- /dev/null +++ b/engine/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/engine/src/main/java/de/gesellix/docker/engine/DockerConfigReader.java b/engine/src/main/java/de/gesellix/docker/engine/DockerConfigReader.java new file mode 100644 index 00000000..414e1257 --- /dev/null +++ b/engine/src/main/java/de/gesellix/docker/engine/DockerConfigReader.java @@ -0,0 +1,79 @@ +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/engine/src/main/java/de/gesellix/docker/engine/DockerEnv.java b/engine/src/main/java/de/gesellix/docker/engine/DockerEnv.java index b55c2ccf..61cf7e5e 100644 --- a/engine/src/main/java/de/gesellix/docker/engine/DockerEnv.java +++ b/engine/src/main/java/de/gesellix/docker/engine/DockerEnv.java @@ -1,5 +1,9 @@ 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; /** @@ -22,11 +26,15 @@ public class DockerEnv { private final String indexUrl_v1 = "https://index.docker.io/v1/"; private final String indexUrl_v2 = "https://registry-1.docker.io"; - private File configFile = new File(System.getProperty("user.home") + "/.docker", "config.json"); + private DockerConfigReader dockerConfigReader; + private DockerContextResolver dockerContextResolver; + + private String contextsDirectoryName = "contexts"; - private File legacyConfigFile = new File(System.getProperty("user.home"), ".dockercfg"); + private File dockerContextStoreDir = null; - private File dockerConfigFile = 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")); @@ -39,51 +47,92 @@ public class DockerEnv { private String officialNotaryServer = "https://notary.docker.io"; public DockerEnv() { - this(getDockerHostOrDefault()); + this(null); } public DockerEnv(String dockerHost) { - this.dockerHost = 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 getDockerHostOrDefault() { + public static String getDockerHostFromSystemPropertyOrEnvironment() { String configuredDockerHost = System.getProperty("docker.host", System.getenv("DOCKER_HOST")); if (configuredDockerHost != null && !configuredDockerHost.isEmpty()) { return configuredDockerHost; } - else { - if (((String) System.getProperties().get("os.name")).toLowerCase().contains("windows")) { - // default to non-tls http - //return "tcp://localhost:2375" - - // or use a named pipe: - return "npipe:////./pipe/docker_engine"; - } - else { - return "unix:///var/run/docker.sock"; - } + 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 void setDockerConfigFile(File dockerConfigFile) { - this.dockerConfigFile = dockerConfigFile; + 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() { - if (dockerConfigFile == null) { - String dockerConfig = System.getProperty("docker.config", System.getenv("DOCKER_CONFIG")); - if (dockerConfig != null && !dockerConfig.isEmpty()) { - this.dockerConfigFile = new File(dockerConfig, "config.json"); - } - else if (configFile.exists()) { - this.dockerConfigFile = configFile; - } - else if (legacyConfigFile.exists()) { - this.dockerConfigFile = legacyConfigFile; - } - } + return dockerConfigReader.getDockerConfigFile(); + } - return dockerConfigFile; + public DockerConfigReader getDockerConfigReader() { + return dockerConfigReader; + } + + public File getDockerContextStoreDir() { + if (dockerContextStoreDir == null) { + dockerContextStoreDir = new File(getDockerConfigFile().getParentFile(), contextsDirectoryName); + } + return dockerContextStoreDir; } public String getDockerHost() { @@ -134,22 +183,6 @@ public String getIndexUrl_v2() { return indexUrl_v2; } - public File getConfigFile() { - return configFile; - } - - public void setConfigFile(File configFile) { - this.configFile = configFile; - } - - public File getLegacyConfigFile() { - return legacyConfigFile; - } - - public void setLegacyConfigFile(File legacyConfigFile) { - this.legacyConfigFile = legacyConfigFile; - } - public String getApiVersion() { return apiVersion; } diff --git a/engine/src/test/groovy/de/gesellix/docker/authentication/AuthConfigReaderTest.groovy b/engine/src/test/groovy/de/gesellix/docker/authentication/AuthConfigReaderTest.groovy index ee70216e..9c93c0f2 100644 --- a/engine/src/test/groovy/de/gesellix/docker/authentication/AuthConfigReaderTest.groovy +++ b/engine/src/test/groovy/de/gesellix/docker/authentication/AuthConfigReaderTest.groovy @@ -1,5 +1,6 @@ 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 @@ -10,10 +11,12 @@ import static de.gesellix.docker.authentication.AuthConfig.EMPTY_AUTH_CONFIG class AuthConfigReaderTest extends Specification { DockerEnv env + DockerConfigReader dockerConfigReader AuthConfigReader authConfigReader def setup() { - env = Mock(DockerEnv) + dockerConfigReader = Spy(DockerConfigReader) + env = Mock(DockerEnv) { it.getDockerConfigReader() >> dockerConfigReader } authConfigReader = Spy(AuthConfigReader, constructorArgs: [env]) } @@ -24,13 +27,13 @@ class AuthConfigReaderTest extends Specification { env.indexUrl_v1 >> 'https://index.docker.io/v1/' when: - def result = authConfigReader.readAuthConfig(null, expectedConfigFile) + 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/") + password: "-yet-another-password-", + email: "tobias@gesellix.de", + serveraddress: "https://index.docker.io/v1/") cleanup: if (oldDockerConfig) { @@ -45,13 +48,13 @@ class AuthConfigReaderTest extends Specification { env.indexUrl_v1 >> 'https://index.docker.io/v1/' when: - def result = authConfigReader.readAuthConfig(null, expectedConfigFile) + 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/") + password: "-yet-another-password-", + email: "tobias@gesellix.de", + serveraddress: "https://index.docker.io/v1/") cleanup: if (oldDockerConfig) { @@ -65,7 +68,7 @@ class AuthConfigReaderTest extends Specification { File dockerCfg = new ResourceReader().getClasspathResourceAsFile('/auth/config.json', AuthConfigReader) when: - def authDetails = authConfigReader.readAuthConfig(null, dockerCfg) + AuthConfig authDetails = authConfigReader.readAuthConfig(null, dockerCfg) then: authDetails.username == "gesellix" @@ -82,7 +85,7 @@ class AuthConfigReaderTest extends Specification { File dockerCfg = new ResourceReader().getClasspathResourceAsFile('/auth/config.json', AuthConfigReader) when: - def authDetails = authConfigReader.readAuthConfig("quay.io", dockerCfg) + AuthConfig authDetails = authConfigReader.readAuthConfig("quay.io", dockerCfg) then: authDetails.username == "gesellix" @@ -100,7 +103,7 @@ class AuthConfigReaderTest extends Specification { assert !nonExistingFile.exists() when: - def authDetails = authConfigReader.readAuthConfig(null, nonExistingFile) + AuthConfig authDetails = authConfigReader.readAuthConfig(null, nonExistingFile) then: authDetails == new AuthConfig() @@ -111,7 +114,7 @@ class AuthConfigReaderTest extends Specification { File dockerCfg = new ResourceReader().getClasspathResourceAsFile('/auth/config.json', AuthConfigReader) when: - def authDetails = authConfigReader.readAuthConfig("unknown.example.com", dockerCfg) + AuthConfig authDetails = authConfigReader.readAuthConfig("unknown.example.com", dockerCfg) then: authDetails == EMPTY_AUTH_CONFIG @@ -127,7 +130,7 @@ class AuthConfigReaderTest extends Specification { env.getDockerConfigFile() >> expectedConfigFile when: - def authConfig = authConfigReader.readDefaultAuthConfig() + AuthConfig authConfig = authConfigReader.readDefaultAuthConfig() then: 1 * authConfigReader.readAuthConfig(null, expectedConfigFile) @@ -146,17 +149,17 @@ class AuthConfigReaderTest extends Specification { String oldDockerConfig = System.clearProperty("docker.config") File expectedConfigFile = new ResourceReader().getClasspathResourceAsFile('/auth/config.json', AuthConfigReader) env.indexUrl_v1 >> 'https://index.docker.io/v1/' - env.getDockerConfigFile() >> expectedConfigFile + dockerConfigReader.getDockerConfigFile() >> expectedConfigFile when: - def result = authConfigReader.readDefaultAuthConfig() + AuthConfig result = authConfigReader.readDefaultAuthConfig() then: 1 * authConfigReader.readAuthConfig(null, expectedConfigFile) result == new AuthConfig(username: "gesellix", - password: "-yet-another-password-", - email: "tobias@gesellix.de", - serveraddress: "https://index.docker.io/v1/") + password: "-yet-another-password-", + email: "tobias@gesellix.de", + serveraddress: "https://index.docker.io/v1/") cleanup: if (oldDockerConfig) { diff --git a/engine/src/test/groovy/de/gesellix/docker/context/DockerContextResolverTest.groovy b/engine/src/test/groovy/de/gesellix/docker/context/DockerContextResolverTest.groovy new file mode 100644 index 00000000..61ede4b8 --- /dev/null +++ b/engine/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/engine/src/test/groovy/de/gesellix/docker/context/MetadataStoreTest.groovy b/engine/src/test/groovy/de/gesellix/docker/context/MetadataStoreTest.groovy new file mode 100644 index 00000000..f0ce3493 --- /dev/null +++ b/engine/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/engine/src/test/groovy/de/gesellix/docker/engine/DockerConfigReaderTest.groovy b/engine/src/test/groovy/de/gesellix/docker/engine/DockerConfigReaderTest.groovy new file mode 100644 index 00000000..9d228ade --- /dev/null +++ b/engine/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/engine/src/test/groovy/de/gesellix/docker/engine/DockerEnvTest.groovy b/engine/src/test/groovy/de/gesellix/docker/engine/DockerEnvTest.groovy index c1ce344f..adc924be 100644 --- a/engine/src/test/groovy/de/gesellix/docker/engine/DockerEnvTest.groovy +++ b/engine/src/test/groovy/de/gesellix/docker/engine/DockerEnvTest.groovy @@ -5,16 +5,11 @@ import spock.lang.Specification class DockerEnvTest extends Specification { - DockerEnv env - - def setup() { - env = new DockerEnv() - } - 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() @@ -25,40 +20,42 @@ class DockerEnvTest extends Specification { cleanup: if (oldDockerConfigDir) { System.setProperty("docker.config", oldDockerConfigDir) - } - else { + } else { System.clearProperty("docker.config") } } def "read default docker config file"() { given: - def oldDockerConfig = System.clearProperty("docker.config") def expectedConfigFile = new ResourceReader().getClasspathResourceAsFile('/auth/config.json', getClass()) - env.configFile = expectedConfigFile + def oldDockerConfigDir = System.setProperty("docker.config", expectedConfigFile.parent) + DockerEnv env = new DockerEnv() when: - def actualConfigFile = env.getDockerConfigFile() + File actualConfigFile = env.getDockerConfigFile() then: actualConfigFile == expectedConfigFile cleanup: - if (oldDockerConfig) { - System.setProperty("docker.config", oldDockerConfig) + if (oldDockerConfigDir) { + System.setProperty("docker.config", oldDockerConfigDir) } } 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.configFile = nonExistingFile + env.dockerConfigReader.configFile = nonExistingFile def expectedConfigFile = new ResourceReader().getClasspathResourceAsFile('/auth/dockercfg', getClass()) - env.legacyConfigFile = expectedConfigFile + env.dockerConfigReader.legacyConfigFile = expectedConfigFile + + env.resetDockerHostFromCurrentConfig() when: def actualConfigFile = env.getDockerConfigFile() diff --git a/engine/src/test/resources/context/config.json b/engine/src/test/resources/context/config.json new file mode 100644 index 00000000..c21c32e1 --- /dev/null +++ b/engine/src/test/resources/context/config.json @@ -0,0 +1,4 @@ +{ + "credsStore": "desktop", + "currentContext": "for-test" +} diff --git a/engine/src/test/resources/context/contexts/meta/297dc204469307b573ca1e71dead5336f61c3aa222bf3a507cd59bf0c07a43b8/meta.json b/engine/src/test/resources/context/contexts/meta/297dc204469307b573ca1e71dead5336f61c3aa222bf3a507cd59bf0c07a43b8/meta.json new file mode 100644 index 00000000..bf1ef630 --- /dev/null +++ b/engine/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/engine/src/test/resources/context/contexts/meta/fe9c6bd7a66301f49ca9b6a70b217107cd1284598bfc254700c989b916da791e/meta.json b/engine/src/test/resources/context/contexts/meta/fe9c6bd7a66301f49ca9b6a70b217107cd1284598bfc254700c989b916da791e/meta.json new file mode 100644 index 00000000..8a2598bf --- /dev/null +++ b/engine/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/engine/src/test/resources/context/docker-default-context.json b/engine/src/test/resources/context/docker-default-context.json new file mode 100644 index 00000000..964148f1 --- /dev/null +++ b/engine/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/integrationtest/src/test/groovy/de/gesellix/docker/engine/TestConstants.groovy b/integrationtest/src/test/groovy/de/gesellix/docker/engine/TestConstants.groovy index 2e21bef7..e60009d5 100644 --- a/integrationtest/src/test/groovy/de/gesellix/docker/engine/TestConstants.groovy +++ b/integrationtest/src/test/groovy/de/gesellix/docker/engine/TestConstants.groovy @@ -19,11 +19,11 @@ class TestConstants { // TODO consider checking the Docker api version instead of "GITHUB_ACTOR" if (LocalDocker.isNativeWindows()) { versionDetails = [ - ApiVersion : { it == "1.42" }, - Arch : { it == "amd64" }, - BuildTime : { it =~ "2022-03-27T\\w+" }, - GitCommit : { it == "8941dcfcc5" }, - GoVersion : { it == "go1.18" }, + ApiVersion : { it == "1.41" }, + Arch : { it in ["amd64", "arm64"] }, + BuildTime : { it =~ "2022-\\d{2}-\\d{2}T\\w+" }, + GitCommit : { it =~ "\\w{6,}" }, + GoVersion : { it == "go1.18.7" }, KernelVersion: { it =~ "\\d.\\d{1,2}.\\d{1,2}\\w*" }, MinAPIVersion: { it == "1.24" }, Os : { it == "windows" }, @@ -32,9 +32,9 @@ class TestConstants { else { versionDetails = [ ApiVersion : { it == "1.41" }, - Arch : { it == "amd64" }, - BuildTime : { it =~ "2022-09-08T\\w+" }, - GitCommit : { it == "e42327a6d3c55ceda3bd5475be7aae6036d02db3" }, + Arch : { it in ["amd64", "arm64"] }, + BuildTime : { it =~ "2022-\\d{2}-\\d{2}T\\w+" }, + GitCommit : { it =~ "\\w{6,}" }, GoVersion : { it == "go1.18.7" }, KernelVersion: { it =~ "\\d.\\d{1,2}.\\d{1,2}\\w*" }, MinAPIVersion: { it == "1.12" }, @@ -45,7 +45,7 @@ class TestConstants { else if (LocalDocker.isNativeWindows()) { versionDetails = [ ApiVersion : { it == "1.41" }, - Arch : { it == "amd64" }, + Arch : { it in ["amd64", "arm64"] }, BuildTime : { it =~ "2022-06-06T\\w+" }, GitCommit : { it == "a89b842" }, GoVersion : { it == "go1.17.11" }, @@ -57,10 +57,10 @@ class TestConstants { else { versionDetails = [ ApiVersion : { it == "1.41" }, - Arch : { it == "amd64" }, - BuildTime : { it =~ "2022-06-06T\\w+" }, - GitCommit : { it == "a89b842" }, - GoVersion : { it == "go1.17.11" }, + Arch : { it in ["amd64", "arm64"] }, + BuildTime : { it =~ "2022-10-18T\\w+" }, + GitCommit : { it == "03df974" }, + GoVersion : { it == "go1.18.7" }, KernelVersion: { it =~ "\\d.\\d{1,2}.\\d{1,2}\\w*" }, MinAPIVersion: { it == "1.12" }, Os : { it == "linux" },