JCLOUDS-1014: Make the login port lookup function configurable

This commit is contained in:
Ignasi Barrera 2015-10-10 00:10:50 +02:00
parent 8ca3a326c8
commit 01d43f503b
9 changed files with 364 additions and 40 deletions

View File

@ -84,6 +84,10 @@
<artifactId>jclouds-okhttp</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-multibindings</artifactId>
</dependency>
<dependency>
<groupId>org.apache.jclouds</groupId>
<artifactId>jclouds-core</artifactId>

View File

@ -16,8 +16,6 @@
*/
package org.jclouds.docker.compute.config;
import com.google.common.base.Function;
import com.google.inject.TypeLiteral;
import org.jclouds.compute.ComputeServiceAdapter;
import org.jclouds.compute.config.ComputeServiceAdapterContextModule;
import org.jclouds.compute.domain.Hardware;
@ -34,6 +32,9 @@ import org.jclouds.docker.domain.State;
import org.jclouds.domain.Location;
import org.jclouds.functions.IdentityFunction;
import com.google.common.base.Function;
import com.google.inject.TypeLiteral;
public class DockerComputeServiceContextModule extends
ComputeServiceAdapterContextModule<Container, Hardware, Image, Location> {
@ -54,6 +55,8 @@ public class DockerComputeServiceContextModule extends
bind(new TypeLiteral<Function<State, NodeMetadata.Status>>() {
}).to(StateToStatus.class);
bind(TemplateOptions.class).to(DockerTemplateOptions.class);
install(new LoginPortLookupModule());
}
}

View File

@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.jclouds.docker.compute.config;
import org.jclouds.docker.compute.functions.LoginPortForContainer;
import com.google.inject.AbstractModule;
import com.google.inject.Binder;
import com.google.inject.multibindings.MapBinder;
public class LoginPortLookupModule extends AbstractModule {
@Override
protected void configure() {
// Declare it to initialize the binder allowing duplicates. Users may
// provide different functions for the same image in different modules, or
// we could provide predefined functions for known images. This allows
// users to set their own ones too.
loginPortLookupBinder(binder());
}
public static MapBinder<String, LoginPortForContainer> loginPortLookupBinder(Binder binder) {
return MapBinder.newMapBinder(binder, String.class, LoginPortForContainer.class).permitDuplicates();
}
}

View File

@ -16,9 +16,6 @@
*/
package org.jclouds.docker.compute.functions;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.getOnlyElement;
import java.net.URI;
import java.util.List;
import java.util.Map;
@ -34,7 +31,6 @@ import org.jclouds.compute.domain.NodeMetadataBuilder;
import org.jclouds.compute.domain.Processor;
import org.jclouds.compute.functions.GroupNamingConvention;
import org.jclouds.docker.domain.Container;
import org.jclouds.docker.domain.Port;
import org.jclouds.docker.domain.State;
import org.jclouds.domain.Location;
import org.jclouds.providers.ProviderMetadata;
@ -54,24 +50,26 @@ public class ContainerToNodeMetadata implements Function<Container, NodeMetadata
* started outside jclouds. Client code should check for this value
* when accessing NodeMetadata from Docker.
*/
public static final Integer NO_LOGIN_PORT = Integer.valueOf(-1);
private static final Integer NO_LOGIN_PORT = Integer.valueOf(-1);
private final ProviderMetadata providerMetadata;
private final Function<State, NodeMetadata.Status> toPortableStatus;
private final GroupNamingConvention nodeNamingConvention;
private final Supplier<Map<String, ? extends Image>> images;
private final Supplier<Set<? extends Location>> locations;
private final LoginPortForContainer loginPortForContainer;
@Inject
public ContainerToNodeMetadata(ProviderMetadata providerMetadata, Function<State,
NodeMetadata.Status> toPortableStatus, GroupNamingConvention.Factory namingConvention,
Supplier<Map<String, ? extends Image>> images,
@Memoized Supplier<Set<? extends Location>> locations) {
this.providerMetadata = checkNotNull(providerMetadata, "providerMetadata");
this.toPortableStatus = checkNotNull(toPortableStatus, "toPortableStatus cannot be null");
this.nodeNamingConvention = checkNotNull(namingConvention, "namingConvention").createWithoutPrefix();
this.images = checkNotNull(images, "images cannot be null");
this.locations = checkNotNull(locations, "locations");
ContainerToNodeMetadata(ProviderMetadata providerMetadata,
Function<State, NodeMetadata.Status> toPortableStatus, GroupNamingConvention.Factory namingConvention,
Supplier<Map<String, ? extends Image>> images, @Memoized Supplier<Set<? extends Location>> locations,
LoginPortForContainer loginPortForContainer) {
this.providerMetadata = providerMetadata;
this.toPortableStatus = toPortableStatus;
this.nodeNamingConvention = namingConvention.createWithoutPrefix();
this.images = images;
this.locations = locations;
this.loginPortForContainer = loginPortForContainer;
}
@Override
@ -90,7 +88,7 @@ public class ContainerToNodeMetadata implements Function<Container, NodeMetadata
.processor(new Processor(container.config().cpuShares(), container.config().cpuShares()))
.build());
builder.status(toPortableStatus.apply(container.state()));
builder.loginPort(getLoginPort(container));
builder.loginPort(loginPortForContainer.apply(container).or(NO_LOGIN_PORT));
builder.publicAddresses(getPublicIpAddresses());
builder.privateAddresses(getPrivateIpAddresses(container));
builder.location(Iterables.getOnlyElement(locations.get()));
@ -117,20 +115,4 @@ public class ContainerToNodeMetadata implements Function<Container, NodeMetadata
return ImmutableList.of(dockerIpAddress);
}
protected static int getLoginPort(Container container) {
if (container.networkSettings() != null) {
Map<String, List<Map<String, String>>> ports = container.networkSettings().ports();
if (ports != null && ports.containsKey("22/tcp")) {
return Integer.parseInt(getOnlyElement(ports.get("22/tcp")).get("HostPort"));
}
// this is needed in case the container list is coming from listContainers
} else if (container.ports() != null) {
for (Port port : container.ports()) {
if (port.privatePort() == 22) {
return port.publicPort();
}
}
}
return NO_LOGIN_PORT;
}
}

View File

@ -0,0 +1,84 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.jclouds.docker.compute.functions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import org.jclouds.docker.domain.Container;
import com.google.common.annotations.Beta;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Maps;
@Beta
public class CustomLoginPortFromImage implements LoginPortForContainer {
private final Map<String, Set<LoginPortForContainer>> imageToPortLookup;
@Inject
CustomLoginPortFromImage(Map<String, Set<LoginPortForContainer>> imageToPortLookup) {
this.imageToPortLookup = imageToPortLookup;
}
@Override
public Optional<Integer> apply(final Container container) {
Map<String, Set<LoginPortForContainer>> matchingFunctions = Maps.filterKeys(imageToPortLookup,
new Predicate<String>() {
@Override
public boolean apply(String input) {
return container.config().image().matches(input);
}
});
// We allow to provide several forms in the image-to-function map:
// - redis
// - redis:12
// - owner/redis:12
// - registry:5000/owner/redis:12
// We consider the longest match first, as it is the more accurate one
List<String> sortedImages = new ArrayList<String>(matchingFunctions.keySet());
Collections.sort(sortedImages, LongestStringFirst);
for (String currentImage : sortedImages) {
Set<LoginPortForContainer> functions = matchingFunctions.get(currentImage);
for (LoginPortForContainer function : functions) {
Optional<Integer> port = function.apply(container);
if (port.isPresent()) {
return port;
}
}
}
return Optional.absent();
}
private static final Comparator<String> LongestStringFirst = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s2.length() - s1.length();
}
};
}

View File

@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.jclouds.docker.compute.functions;
import javax.inject.Inject;
import org.jclouds.docker.compute.functions.LoginPortForContainer.LoginPortLookupChain;
import org.jclouds.docker.domain.Container;
import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.inject.ImplementedBy;
@Beta
@ImplementedBy(LoginPortLookupChain.class)
public interface LoginPortForContainer extends Function<Container, Optional<Integer>> {
@Beta
static final class LoginPortLookupChain implements LoginPortForContainer {
private final PublicPortForContainerPort publicPortForContainerPort;
private final CustomLoginPortFromImage customLoginPortFromImage;
@Inject
LoginPortLookupChain(CustomLoginPortFromImage customLoginPortFromImage) {
this.publicPortForContainerPort = new PublicPortForContainerPort(22);
this.customLoginPortFromImage = customLoginPortFromImage;
}
@Override
public Optional<Integer> apply(Container input) {
Optional<Integer> loginPort = publicPortForContainerPort.apply(input);
return loginPort.isPresent() ? loginPort : customLoginPortFromImage.apply(input);
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.jclouds.docker.compute.functions;
import static com.google.common.collect.Iterables.getOnlyElement;
import java.util.List;
import java.util.Map;
import org.jclouds.docker.domain.Container;
import org.jclouds.docker.domain.Port;
import com.google.common.annotations.Beta;
import com.google.common.base.Optional;
@Beta
public class PublicPortForContainerPort implements LoginPortForContainer {
private final int containerPort;
public PublicPortForContainerPort(int containerPort) {
this.containerPort = containerPort;
}
@Override
public Optional<Integer> apply(Container container) {
if (container.networkSettings() != null) {
Map<String, List<Map<String, String>>> ports = container.networkSettings().ports();
if (ports != null && ports.containsKey(containerPort + "/tcp")) {
return Optional.of(Integer.parseInt(getOnlyElement(ports.get(containerPort + "/tcp")).get("HostPort")));
}
// this is needed in case the container list is coming from
// listContainers
} else if (container.ports() != null) {
for (Port port : container.ports()) {
if (port.privatePort() == containerPort) {
return Optional.of(port.publicPort());
}
}
}
return Optional.absent();
}
}

View File

@ -44,7 +44,6 @@ import org.jclouds.docker.domain.State;
import org.jclouds.domain.Location;
import org.jclouds.domain.LocationBuilder;
import org.jclouds.domain.LocationScope;
import org.jclouds.domain.LoginCredentials;
import org.jclouds.providers.ProviderMetadata;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@ -62,8 +61,6 @@ import com.google.inject.Guice;
@Test(groups = "unit", testName = "ContainerToNodeMetadataTest")
public class ContainerToNodeMetadataTest {
private LoginCredentials credentials;
private ContainerToNodeMetadata function;
private Container container;
@ -170,9 +167,8 @@ public class ContainerToNodeMetadataTest {
}
};
credentials = LoginCredentials.builder().user("foo").password("bar").build();
function = new ContainerToNodeMetadata(providerMetadata, toPortableStatus(), namingConvention, images, locations);
function = new ContainerToNodeMetadata(providerMetadata, toPortableStatus(), namingConvention, images, locations,
new LoginPortForContainer.LoginPortLookupChain(null));
}
private Function<State, NodeMetadata.Status> toPortableStatus() {

View File

@ -0,0 +1,106 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.jclouds.docker.compute.functions;
import static org.jclouds.docker.compute.config.LoginPortLookupModule.loginPortLookupBinder;
import static org.testng.Assert.assertEquals;
import org.jclouds.docker.compute.config.LoginPortLookupModule;
import org.jclouds.docker.domain.Config;
import org.jclouds.docker.domain.Container;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.multibindings.MapBinder;
@Test(groups = "unit")
public class CustomLoginPortFromImageTest {
private CustomLoginPortFromImage customLoginPortFromImage;
@BeforeClass
public void setup() {
Injector i = Guice.createInjector(new LoginPortLookupModule(), new AbstractModule() {
@Override
protected void configure() {
MapBinder<String, LoginPortForContainer> imageToFunction = loginPortLookupBinder(binder());
imageToFunction.addBinding(".*alpine-ext.*").toInstance(LoginPortFromEnvVar);
imageToFunction.addBinding(".*ubuntu.*").toInstance(AlwaysPort22);
imageToFunction.addBinding(".*ubuntu:12\\.04.*").toInstance(AlwaysPort8080);
}
});
customLoginPortFromImage = i.getInstance(CustomLoginPortFromImage.class);
}
public void testPortFromEnvironmentVariables() {
Config config = Config.builder().image("alpine-ext:3.2").env(ImmutableList.of("FOO=bar", "SSH_PORT=2345"))
.build();
Container container = Container.builder().id("id").config(config).build();
assertEquals(customLoginPortFromImage.apply(container).get().intValue(), 2345);
}
public void testMostSpecificImageIsPicked() {
Config config = Config.builder().image("ubuntu:12.04").build();
Container container = Container.builder().id("id").config(config).build();
assertEquals(customLoginPortFromImage.apply(container).get().intValue(), 8080);
}
public void testNoImageFoundInMap() {
Config config = Config.builder().image("unexisting").build();
Container container = Container.builder().id("id").config(config).build();
assertEquals(customLoginPortFromImage.apply(container), Optional.absent());
}
private static final LoginPortForContainer LoginPortFromEnvVar = new LoginPortForContainer() {
@Override
public Optional<Integer> apply(Container input) {
Optional<String> portVariable = Iterables.tryFind(input.config().env(), new Predicate<String>() {
@Override
public boolean apply(String input) {
String[] var = input.split("=");
return var[0].equals("SSH_PORT");
}
});
return portVariable.isPresent() ? Optional.of(Integer.valueOf(portVariable.get().split("=")[1])) : Optional
.<Integer> absent();
}
};
private static final LoginPortForContainer AlwaysPort22 = new LoginPortForContainer() {
@Override
public Optional<Integer> apply(Container input) {
return Optional.of(22);
}
};
private static final LoginPortForContainer AlwaysPort8080 = new LoginPortForContainer() {
@Override
public Optional<Integer> apply(Container input) {
return Optional.of(8080);
}
};
}