-
Notifications
You must be signed in to change notification settings - Fork 90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve ports parsing #418
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/* | ||
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.palantir.docker.compose.execution; | ||
|
||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.hamcrest.core.Is.is; | ||
|
||
import com.google.common.collect.ImmutableList; | ||
import com.google.common.collect.ImmutableSet; | ||
import com.palantir.docker.compose.DockerComposeManager; | ||
import com.palantir.docker.compose.configuration.ShutdownStrategy; | ||
import com.palantir.docker.compose.connection.DockerPort; | ||
import com.palantir.docker.compose.connection.Ports; | ||
import java.io.IOException; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
import org.junit.AfterClass; | ||
import org.junit.BeforeClass; | ||
import org.junit.Test; | ||
|
||
public class DockerComposePortsIntegrationTest { | ||
|
||
private static final String LOCALHOST_IP = "127.0.0.1"; | ||
|
||
private static DockerComposeManager dockerComposeManager = new DockerComposeManager.Builder() | ||
.shutdownStrategy(ShutdownStrategy.KILL_DOWN) | ||
.file("src/test/resources/ports-docker-compose.yaml") | ||
.build(); | ||
|
||
@BeforeClass | ||
public static void beforeClass() throws IOException, InterruptedException { | ||
dockerComposeManager.before(); | ||
} | ||
|
||
@Test | ||
public void no_ports_mapped() throws IOException, InterruptedException { | ||
Ports ports = dockerComposeManager.dockerCompose().ports("no-ports-mapped"); | ||
Ports expectedPorts = new Ports(ImmutableList.of()); | ||
assertThat(ports, is(expectedPorts)); | ||
} | ||
|
||
@Test | ||
public void ports_mapped_identically() throws IOException, InterruptedException { | ||
Ports ports = dockerComposeManager.dockerCompose().ports("ports-mapped-identically"); | ||
Ports expectedPorts = new Ports(ImmutableList.of( | ||
new DockerPort(LOCALHOST_IP, 5432, 5432))); | ||
assertThat(ports, is(expectedPorts)); | ||
} | ||
|
||
@Test | ||
public void ports_mapped_differently() throws IOException, InterruptedException { | ||
Ports ports = dockerComposeManager.dockerCompose().ports("ports-mapped-differently"); | ||
Ports expectedPorts = new Ports(ImmutableList.of( | ||
new DockerPort(LOCALHOST_IP, 1234, 5678))); | ||
assertThat(ports, is(expectedPorts)); | ||
} | ||
|
||
@Test | ||
public void ports_mapped_with_different_ip() throws IOException, InterruptedException { | ||
Ports ports = dockerComposeManager.dockerCompose().ports("ports-mapped-with-different-ip"); | ||
Ports expectedPorts = new Ports(ImmutableList.of( | ||
new DockerPort(LOCALHOST_IP, 8000, 8000))); | ||
assertThat(ports, is(expectedPorts)); | ||
} | ||
|
||
@Test | ||
public void ports_exposed_but_not_mapped() throws IOException, InterruptedException { | ||
Ports ports = dockerComposeManager.dockerCompose().ports("ports-exposed-but-not-mapped"); | ||
Ports expectedPorts = new Ports(ImmutableList.of()); | ||
assertThat(ports, is(expectedPorts)); | ||
} | ||
|
||
@Test | ||
public void lots_of_port_information() throws IOException, InterruptedException { | ||
Ports ports = dockerComposeManager.dockerCompose().ports("lots-of-port-information"); | ||
Set<DockerPort> expectedPortsSet = ImmutableSet.of( | ||
new DockerPort(LOCALHOST_IP, 9000, 9000), | ||
new DockerPort(LOCALHOST_IP, 9010, 9010), | ||
new DockerPort(LOCALHOST_IP, 9020, 9020), | ||
new DockerPort(LOCALHOST_IP, 6666, 7777), | ||
new DockerPort(LOCALHOST_IP, 7777, 8888), | ||
new DockerPort(LOCALHOST_IP, 8888, 9999), | ||
new DockerPort(LOCALHOST_IP, 4000, 4000), | ||
new DockerPort(LOCALHOST_IP, 4010, 4010), | ||
new DockerPort(LOCALHOST_IP, 4020, 4020)); | ||
assertThat(ports.stream().collect(Collectors.toSet()), is(expectedPortsSet)); | ||
} | ||
|
||
@AfterClass | ||
public static void afterClass() { | ||
dockerComposeManager.after(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,8 +19,10 @@ | |
import static org.joda.time.Duration.standardMinutes; | ||
|
||
import com.github.zafarkhaja.semver.Version; | ||
import com.google.common.base.Joiner; | ||
import com.google.common.base.Strings; | ||
import com.google.common.collect.ImmutableList; | ||
import com.google.common.collect.Lists; | ||
import com.palantir.docker.compose.configuration.DockerComposeFiles; | ||
import com.palantir.docker.compose.configuration.ProjectName; | ||
import com.palantir.docker.compose.connection.Container; | ||
|
@@ -45,102 +47,111 @@ public class DefaultDockerCompose implements DockerCompose { | |
private static final Duration LOG_TIMEOUT = standardMinutes(1); | ||
private static final Logger log = LoggerFactory.getLogger(DefaultDockerCompose.class); | ||
|
||
private final Command command; | ||
private final Command dockerComposeCommand; | ||
private final DockerComposeExecutable dockerComposeRawExecutable; | ||
private final Command dockerCommand; | ||
private final DockerMachine dockerMachine; | ||
private final DockerComposeExecutable rawExecutable; | ||
|
||
|
||
public DefaultDockerCompose(DockerComposeFiles dockerComposeFiles, DockerMachine dockerMachine, ProjectName projectName) { | ||
this(DockerComposeExecutable.builder() | ||
.dockerComposeFiles(dockerComposeFiles) | ||
.dockerConfiguration(dockerMachine) | ||
.projectName(projectName) | ||
.build(), dockerMachine); | ||
.build(), | ||
DockerExecutable.builder() | ||
.dockerConfiguration(dockerMachine) | ||
.build(), | ||
dockerMachine); | ||
} | ||
|
||
public DefaultDockerCompose(DockerComposeExecutable rawExecutable, DockerMachine dockerMachine) { | ||
this.rawExecutable = rawExecutable; | ||
this.command = new Command(rawExecutable, log::trace); | ||
public DefaultDockerCompose( | ||
DockerComposeExecutable dockerComposeRawExecutable, | ||
DockerExecutable dockerRawExecutable, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also seems safe from a brief github search (I think 1 person has used it externally and none internally) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah it seems reasonable enough, will add comments about break-impact next time! |
||
DockerMachine dockerMachine) { | ||
this.dockerComposeRawExecutable = dockerComposeRawExecutable; | ||
this.dockerComposeCommand = new Command(dockerComposeRawExecutable, log::trace); | ||
this.dockerCommand = new Command(dockerRawExecutable, log::trace); | ||
this.dockerMachine = dockerMachine; | ||
} | ||
|
||
@Override | ||
public void pull() throws IOException, InterruptedException { | ||
command.execute(Command.throwingOnError(), "pull"); | ||
dockerComposeCommand.execute(Command.throwingOnError(), "pull"); | ||
} | ||
|
||
@Override | ||
public void build() throws IOException, InterruptedException { | ||
command.execute(Command.throwingOnError(), "build"); | ||
dockerComposeCommand.execute(Command.throwingOnError(), "build"); | ||
} | ||
|
||
@Override | ||
public void up() throws IOException, InterruptedException { | ||
command.execute(Command.throwingOnError(), "up", "-d"); | ||
dockerComposeCommand.execute(Command.throwingOnError(), "up", "-d"); | ||
} | ||
|
||
@Override | ||
public void down() throws IOException, InterruptedException { | ||
command.execute(swallowingDownCommandDoesNotExist(), "down", "--volumes"); | ||
dockerComposeCommand.execute(swallowingDownCommandDoesNotExist(), "down", "--volumes"); | ||
} | ||
|
||
@Override | ||
public void stop() throws IOException, InterruptedException { | ||
command.execute(Command.throwingOnError(), "stop"); | ||
dockerComposeCommand.execute(Command.throwingOnError(), "stop"); | ||
|
||
} | ||
|
||
@Override | ||
public void kill() throws IOException, InterruptedException { | ||
command.execute(Command.throwingOnError(), "kill"); | ||
dockerComposeCommand.execute(Command.throwingOnError(), "kill"); | ||
} | ||
|
||
@Override | ||
public void rm() throws IOException, InterruptedException { | ||
command.execute(Command.throwingOnError(), "rm", "--force", "-v"); | ||
dockerComposeCommand.execute(Command.throwingOnError(), "rm", "--force", "-v"); | ||
} | ||
|
||
@Override | ||
public void up(Container container) throws IOException, InterruptedException { | ||
command.execute(Command.throwingOnError(), "up", "-d", container.getContainerName()); | ||
dockerComposeCommand.execute(Command.throwingOnError(), "up", "-d", container.getContainerName()); | ||
} | ||
|
||
@Override | ||
public void start(Container container) throws IOException, InterruptedException { | ||
command.execute(Command.throwingOnError(), "start", container.getContainerName()); | ||
dockerComposeCommand.execute(Command.throwingOnError(), "start", container.getContainerName()); | ||
} | ||
|
||
@Override | ||
public void stop(Container container) throws IOException, InterruptedException { | ||
command.execute(Command.throwingOnError(), "stop", container.getContainerName()); | ||
dockerComposeCommand.execute(Command.throwingOnError(), "stop", container.getContainerName()); | ||
} | ||
|
||
@Override | ||
public void kill(Container container) throws IOException, InterruptedException { | ||
command.execute(Command.throwingOnError(), "kill", container.getContainerName()); | ||
dockerComposeCommand.execute(Command.throwingOnError(), "kill", container.getContainerName()); | ||
} | ||
|
||
@Override | ||
public String exec(DockerComposeExecOption dockerComposeExecOption, String containerName, | ||
DockerComposeExecArgument dockerComposeExecArgument) throws IOException, InterruptedException { | ||
verifyDockerComposeVersionAtLeast(VERSION_1_7_0, "You need at least docker-compose 1.7 to run docker-compose exec"); | ||
String[] fullArgs = constructFullDockerComposeExecArguments(dockerComposeExecOption, containerName, dockerComposeExecArgument); | ||
return command.execute(Command.throwingOnError(), fullArgs); | ||
return dockerComposeCommand.execute(Command.throwingOnError(), fullArgs); | ||
} | ||
|
||
@Override | ||
public String run(DockerComposeRunOption dockerComposeRunOption, String containerName, | ||
DockerComposeRunArgument dockerComposeRunArgument) throws IOException, InterruptedException { | ||
String[] fullArgs = constructFullDockerComposeRunArguments(dockerComposeRunOption, containerName, dockerComposeRunArgument); | ||
return command.execute(Command.throwingOnError(), fullArgs); | ||
return dockerComposeCommand.execute(Command.throwingOnError(), fullArgs); | ||
} | ||
|
||
private void verifyDockerComposeVersionAtLeast(Version targetVersion, String message) throws IOException, InterruptedException { | ||
validState(version().greaterThanOrEqualTo(targetVersion), message); | ||
} | ||
|
||
private Version version() throws IOException, InterruptedException { | ||
String versionOutput = command.execute(Command.throwingOnError(), "-v"); | ||
String versionOutput = dockerComposeCommand.execute(Command.throwingOnError(), "-v"); | ||
return DockerComposeVersion.parseFromDockerComposeVersion(versionOutput); | ||
} | ||
|
||
|
@@ -170,7 +181,7 @@ private static String[] constructFullDockerComposeRunArguments(DockerComposeRunO | |
|
||
@Override | ||
public List<ContainerName> ps() throws IOException, InterruptedException { | ||
String psOutput = command.execute(Command.throwingOnError(), "ps"); | ||
String psOutput = dockerComposeCommand.execute(Command.throwingOnError(), "ps"); | ||
return ContainerNames.parseFromDockerComposePs(psOutput); | ||
} | ||
|
||
|
@@ -181,12 +192,12 @@ public Optional<String> id(Container container) throws IOException, InterruptedE | |
|
||
@Override | ||
public String config() throws IOException, InterruptedException { | ||
return command.execute(Command.throwingOnError(), "config"); | ||
return dockerComposeCommand.execute(Command.throwingOnError(), "config"); | ||
} | ||
|
||
@Override | ||
public List<String> services() throws IOException, InterruptedException { | ||
String servicesOutput = command.execute(Command.throwingOnError(), "config", "--services"); | ||
String servicesOutput = dockerComposeCommand.execute(Command.throwingOnError(), "config", "--services"); | ||
return Arrays.asList(servicesOutput.split("(\r|\n)+")); | ||
} | ||
|
||
|
@@ -210,7 +221,7 @@ public boolean writeLogs(String container, OutputStream output) throws IOExcepti | |
} | ||
|
||
private Optional<String> id(String containerName) throws IOException, InterruptedException { | ||
String id = command.execute(Command.throwingOnError(), "ps", "-q", containerName); | ||
String id = dockerComposeCommand.execute(Command.throwingOnError(), "ps", "-q", containerName); | ||
if (id.isEmpty()) { | ||
return Optional.empty(); | ||
} | ||
|
@@ -220,12 +231,20 @@ private Optional<String> id(String containerName) throws IOException, Interrupte | |
private Process logs(String container) throws IOException, InterruptedException { | ||
verifyDockerComposeVersionAtLeast(VERSION_1_7_0, | ||
"You need at least docker-compose 1.7 to run docker-compose logs"); | ||
return rawExecutable.execute("logs", "--no-color", container); | ||
return dockerComposeRawExecutable.execute("logs", "--no-color", container); | ||
} | ||
|
||
@Override | ||
public Ports ports(String service) throws IOException, InterruptedException { | ||
return Ports.parseFromDockerComposePs(psOutput(service), dockerMachine.getIp()); | ||
validState(!Strings.isNullOrEmpty(service), String.format("Service cannot be empty")); | ||
|
||
String containerId = dockerComposeCommand.execute(swallowingServiceDoesNotExist(), "ps", "-q", service); | ||
validState(!Strings.isNullOrEmpty(containerId), String.format("No container ID found for service with name '%s'.", service)); | ||
|
||
String portInformation = dockerCommand.execute(Command.throwingOnError(), | ||
"ps", "--no-trunc", "--format", "{{.Ports}}", "--filter", String.format("id=%s", containerId)); | ||
|
||
return Ports.parseFromPortInformation(portInformation, dockerMachine.getIp()); | ||
} | ||
|
||
private static ErrorHandler swallowingDownCommandDoesNotExist() { | ||
|
@@ -244,9 +263,20 @@ private static boolean downCommandWasPresent(String output) { | |
return !output.contains("No such command"); | ||
} | ||
|
||
private String psOutput(String service) throws IOException, InterruptedException { | ||
String psOutput = command.execute(Command.throwingOnError(), "ps", service); | ||
validState(!Strings.isNullOrEmpty(psOutput), "No container with name '" + service + "' found"); | ||
return psOutput; | ||
private static ErrorHandler swallowingServiceDoesNotExist() { | ||
return (exitCode, output, commandName, commands) -> { | ||
if (exitCode == 1 && output.isEmpty()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why exit code 1 and not all exit codes? Will this silently fail if another exit code is thrown? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this is pretty specific, its really only trying to add debug information for when docker can't find a service
Seems it would be more proper for this function to check for various failure exit codes and throw an exception explicitly, while still providing output with suggestions around what the potential failure mode was? |
||
// Note: This (badly) checks if the command most likely failed due to | ||
// the service not existing. If the ErrorHandler had access to | ||
// error output, the proper way to check this would be to inspect | ||
// the error output for "ERROR: No such service: <service_name>" | ||
|
||
String fullCommand = Joiner.on(" ").join(Lists.asList(commandName, commands)); | ||
String serviceName = commands[commands.length - 1]; | ||
|
||
log.warn("It looks like `{}` failed.", fullCommand); | ||
log.warn("This probably happened because no `{}` service exists.", serviceName); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this throw rather than silently continue? I imagine we can't recover from this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If that output is produced, the command will have failed with empty output and then will trigger an exception right after it here: https://github.com/palantir/docker-compose-rule/pull/418/files#diff-cf7a91b7706a65a0c4cc650e69cf5d81R242 I agree that maybe a little misleading and we could clarify how |
||
} | ||
}; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately nearly everything in DCR is public rather than package-private - so we should do some quick github searches to find out if anyone is using this internally or externally before making such a breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fortunately in this case it does look safe, but worth bearing in mind!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, missed that one, sorry. Can include explicit comments about usage / impact of the break in the future