JCLOUDS-331 - support specifying an imageChooser function in TemplateBuilder

This commit is contained in:
Alex Heneveld 2013-10-02 05:48:32 -07:00 committed by Andrew Phillips
parent aa8fab16f9
commit 8207c53cf2
4 changed files with 204 additions and 26 deletions

View File

@ -91,6 +91,11 @@ public class EC2TemplateBuilderImplTest extends TemplateBuilderImplTest {
optionsProvider, templateBuilderProvider, Suppliers.<LoadingCache<RegionAndName, ? extends Image>>ofInstance(imageMap)); optionsProvider, templateBuilderProvider, Suppliers.<LoadingCache<RegionAndName, ? extends Image>>ofInstance(imageMap));
} }
@Override
protected String getProviderFormatId(String uniqueLabel) {
return "us-east-1/"+uniqueLabel;
}
@Override @Override
@Test @Test

View File

@ -16,10 +16,13 @@
*/ */
package org.jclouds.compute.domain; package org.jclouds.compute.domain;
import java.util.NoSuchElementException;
import org.jclouds.compute.domain.internal.TemplateBuilderImpl; import org.jclouds.compute.domain.internal.TemplateBuilderImpl;
import org.jclouds.compute.options.TemplateOptions; import org.jclouds.compute.options.TemplateOptions;
import com.google.common.annotations.Beta; import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.inject.ImplementedBy; import com.google.inject.ImplementedBy;
@ -157,7 +160,7 @@ public interface TemplateBuilder {
TemplateBuilder imageDescriptionMatches(String imageDescriptionRegex); TemplateBuilder imageDescriptionMatches(String imageDescriptionRegex);
/** /**
* Configure this template to have an image description that matches the supplied condition * Configure this template to have an image that matches the supplied condition
* *
* ex. * ex.
* *
@ -167,6 +170,15 @@ public interface TemplateBuilder {
*/ */
TemplateBuilder imageMatches(Predicate<Image> condition); TemplateBuilder imageMatches(Predicate<Image> condition);
/**
* Configure this template with a specific preference function which operates on
* images which match the other criteria.
* <p>
* If no function is supplied, jclouds will select one according to an internal strategy.
* This strategy may change from version to version.
*/
public TemplateBuilderImpl imageChooser(Function<Iterable<? extends Image>,Image> imageChooser);
/** /**
* Configure this template to require the minimum cores below * Configure this template to require the minimum cores below
*/ */

View File

@ -115,6 +115,8 @@ public class TemplateBuilderImpl implements TemplateBuilder {
@VisibleForTesting @VisibleForTesting
protected Predicate<Image> imagePredicate; protected Predicate<Image> imagePredicate;
@VisibleForTesting @VisibleForTesting
protected Function<Iterable<? extends Image>, Image> imageChooser;
@VisibleForTesting
protected double minCores; protected double minCores;
@VisibleForTesting @VisibleForTesting
protected int minRam; protected int minRam;
@ -482,6 +484,13 @@ public class TemplateBuilderImpl implements TemplateBuilder {
}; };
static final Ordering<Image> DEFAULT_IMAGE_ORDERING = new Ordering<Image>() { static final Ordering<Image> DEFAULT_IMAGE_ORDERING = new Ordering<Image>() {
public int compare(Image left, Image right) { public int compare(Image left, Image right) {
/* This currently, and for some time, has *preferred* images whose fields are null,
* and prefers those which come last alphabetically.
* It seems preferable to take images whose fields are *not* null, ie nullsFirst;
* and to use something like the AlphaNum Algorithm then take the last
* (so "Ubuntu 13.04" would be preferred over "Ubuntu 9.10").
* However not changing it now as people may be surprised if the images they get back start changing.
*/
return ComparisonChain.start() return ComparisonChain.start()
.compare(left.getName(), right.getName(), Ordering.<String> natural().nullsLast()) .compare(left.getName(), right.getName(), Ordering.<String> natural().nullsLast())
.compare(left.getVersion(), right.getVersion(), Ordering.<String> natural().nullsLast()) .compare(left.getVersion(), right.getVersion(), Ordering.<String> natural().nullsLast())
@ -497,6 +506,25 @@ public class TemplateBuilderImpl implements TemplateBuilder {
} }
}; };
@VisibleForTesting
// non-static for logging
final Function<Iterable<? extends Image>, Image> imageChooserFromOrdering(final Ordering<Image> ordering) {
return new Function<Iterable<? extends Image>, Image>() {
@Override
public Image apply(Iterable<? extends Image> input) {
List<? extends Image> maxImages = multiMax(ordering, input);
if (logger.isTraceEnabled())
logger.trace("<< best images(%s)", transform(maxImages, imageToId));
return maxImages.get(maxImages.size() - 1);
}
};
}
@VisibleForTesting
Function<Iterable<? extends Image>, Image> defaultImageChooser() {
return imageChooserFromOrdering(DEFAULT_IMAGE_ORDERING);
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -765,6 +793,11 @@ public class TemplateBuilderImpl implements TemplateBuilder {
return hardware; return hardware;
} }
protected Function<Iterable<? extends Image>, Image> imageChooser() {
if (imageChooser != null) return imageChooser;
return defaultImageChooser();
}
protected Ordering<Hardware> hardwareSorter() { protected Ordering<Hardware> hardwareSorter() {
Ordering<Hardware> hardwareOrdering = DEFAULT_SIZE_ORDERING; Ordering<Hardware> hardwareOrdering = DEFAULT_SIZE_ORDERING;
if (!biggest) if (!biggest)
@ -799,10 +832,7 @@ public class TemplateBuilderImpl implements TemplateBuilder {
Iterable<? extends Image> matchingImages = filter(supportedImages, imagePredicate); Iterable<? extends Image> matchingImages = filter(supportedImages, imagePredicate);
if (logger.isTraceEnabled()) if (logger.isTraceEnabled())
logger.trace("<< matched images(%s)", transform(matchingImages, imageToId)); logger.trace("<< matched images(%s)", transform(matchingImages, imageToId));
List<? extends Image> maxImages = multiMax(DEFAULT_IMAGE_ORDERING, matchingImages); return imageChooser().apply(matchingImages);
if (logger.isTraceEnabled())
logger.trace("<< best images(%s)", transform(maxImages, imageToId));
return maxImages.get(maxImages.size() - 1);
} catch (NoSuchElementException exception) { } catch (NoSuchElementException exception) {
throwNoSuchElementExceptionAfterLoggingImageIds(format("no image matched params: %s", toString()), throwNoSuchElementExceptionAfterLoggingImageIds(format("no image matched params: %s", toString()),
supportedImages); supportedImages);
@ -941,6 +971,15 @@ public class TemplateBuilderImpl implements TemplateBuilder {
return this; return this;
} }
/**
* {@inheritDoc}
*/
@Override
public TemplateBuilderImpl imageChooser(Function<Iterable<? extends Image>,Image> imageChooser) {
this.imageChooser = imageChooser;
return this;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -1045,9 +1084,9 @@ public class TemplateBuilderImpl implements TemplateBuilder {
@VisibleForTesting @VisibleForTesting
boolean nothingChangedExceptOptions() { boolean nothingChangedExceptOptions() {
return osFamily == null && location == null && imageId == null && hardwareId == null && hypervisor == null return osFamily == null && location == null && imageId == null && hardwareId == null && hypervisor == null
&& osName == null && imagePredicate == null && osDescription == null && imageVersion == null && osName == null && imagePredicate == null && imageChooser == null && osDescription == null
&& osVersion == null && osArch == null && os64Bit == null && imageName == null && imageDescription == null && imageVersion == null && osVersion == null && osArch == null && os64Bit == null && imageName == null
&& minCores == 0 && minRam == 0 && minDisk == 0 && !biggest && !fastest; && imageDescription == null && minCores == 0 && minRam == 0 && minDisk == 0 && !biggest && !fastest;
} }
/** /**
@ -1076,6 +1115,7 @@ public class TemplateBuilderImpl implements TemplateBuilder {
toString.add("imageDescription", imageDescription); toString.add("imageDescription", imageDescription);
toString.add("imageId", imageId); toString.add("imageId", imageId);
toString.add("imagePredicate", imagePredicate); toString.add("imagePredicate", imagePredicate);
toString.add("imageChooser", imageChooser);
toString.add("imageVersion", imageVersion); toString.add("imageVersion", imageVersion);
if (location != null) if (location != null)
toString.add("locationId", location.getId()); toString.add("locationId", location.getId());

View File

@ -21,8 +21,10 @@ import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify; import static org.easymock.EasyMock.verify;
import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail; import static org.testng.Assert.fail;
import java.util.Arrays;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Set; import java.util.Set;
@ -46,10 +48,15 @@ import org.jclouds.domain.LocationBuilder;
import org.jclouds.domain.LocationScope; import org.jclouds.domain.LocationScope;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier; import com.google.common.base.Supplier;
import com.google.common.base.Suppliers; import com.google.common.base.Suppliers;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering; import com.google.common.collect.Ordering;
/** /**
@ -69,6 +76,24 @@ public class TemplateBuilderImplTest {
assertEquals(TemplateBuilderImpl.multiMax(Ordering.natural(), values), ImmutableList.of("3")); assertEquals(TemplateBuilderImpl.multiMax(Ordering.natural(), values), ImmutableList.of("3"));
} }
public void testMultiMax2() {
// check with max buried in the middle
Iterable<String> values = ImmutableList.of("1", "3", "2", "2");
assertEquals(TemplateBuilderImpl.multiMax(Ordering.natural(), values), ImmutableList.of("3"));
}
public void testMultiMaxNull() {
// we rely on checking nulls in some Orderings, so assert it also does what we expect
// (unfortunately can't use ImmutableList here as that doesn't allow nulls)
Iterable<String> values = Arrays.asList("1", "3", null, "2", "2");
assertEquals(TemplateBuilderImpl.multiMax(Ordering.natural().nullsLast(), values), Arrays.asList((Object)null));
}
public void testMultiMaxNulls() {
Iterable<String> values = Arrays.asList("1", "3", null, "2", "2", null);
assertEquals(TemplateBuilderImpl.multiMax(Ordering.natural().nullsLast(), values), Arrays.asList((Object)null, null));
}
protected Location provider = new LocationBuilder().scope(LocationScope.PROVIDER).id("aws-ec2").description("aws-ec2").build(); protected Location provider = new LocationBuilder().scope(LocationScope.PROVIDER).id("aws-ec2").description("aws-ec2").build();
protected Location region = new LocationBuilder().scope(LocationScope.REGION).id("us-east-1") protected Location region = new LocationBuilder().scope(LocationScope.REGION).id("us-east-1")
@ -80,11 +105,22 @@ public class TemplateBuilderImplTest {
protected OperatingSystem os = OperatingSystem.builder().name("osName").version("osVersion") protected OperatingSystem os = OperatingSystem.builder().name("osName").version("osVersion")
.description("osDescription").arch("X86_32").build(); .description("osDescription").arch("X86_32").build();
protected Image image = new ImageBuilder().id("us-east-1/imageId").providerId("imageId").name("imageName") protected String getProviderFormatId(String uniqueLabel) {
return uniqueLabel;
}
protected Image image = new ImageBuilder().id(getProviderFormatId("imageId")).providerId("imageId").name("imageName")
.description("imageDescription").version("imageVersion").operatingSystem(os).status(Image.Status.AVAILABLE) .description("imageDescription").version("imageVersion").operatingSystem(os).status(Image.Status.AVAILABLE)
.location(region).build(); .location(region).build();
protected Image image2 = ImageBuilder.fromImage(image).operatingSystem(os.toBuilder().arch("X86_64").build()).build(); protected Image image64bit = ImageBuilder.fromImage(image).id(getProviderFormatId("image64bId")).providerId("image64bId")
.operatingSystem(os.toBuilder().arch("X86_64").build()).build();
protected Image imageArchNull = ImageBuilder.fromImage(image).id(getProviderFormatId("imageArchNullId")).providerId("imageArchNullId")
.operatingSystem(os.toBuilder().arch(null).build()).build();
protected Image imageNameAlt = ImageBuilder.fromImage(image).id(getProviderFormatId("imageNameAltId")).providerId("imageNameAltId")
.name("alternateImageName").build();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void testLocationPredicateWhenComputeMetadataIsNotLocationBound() { public void testLocationPredicateWhenComputeMetadataIsNotLocationBound() {
@ -111,16 +147,12 @@ public class TemplateBuilderImplTest {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Test protected void doTestResolveImages(Supplier<Set<? extends Image>> images, Image expectedBest,
public void testResolveImages() { Function<TemplateBuilderImpl, TemplateBuilderImpl> builderCustomisation) {
Hardware hardware = new HardwareBuilder().id("hardwareId").build(); Hardware hardware = new HardwareBuilder().id("hardwareId").build();
Supplier<Set<? extends Location>> locations = Suppliers.<Set<? extends Location>> ofInstance(ImmutableSet Supplier<Set<? extends Location>> locations = Suppliers.<Set<? extends Location>> ofInstance(ImmutableSet
.<Location> of(region)); .<Location> of(region));
Supplier<Set<? extends Image>> images = Suppliers.<Set<? extends Image>> ofInstance(ImmutableSet.<Image> of(
image, image2));
Supplier<Set<? extends Hardware>> hardwares = Suppliers.<Set<? extends Hardware>> ofInstance(ImmutableSet Supplier<Set<? extends Hardware>> hardwares = Suppliers.<Set<? extends Hardware>> ofInstance(ImmutableSet
.<Hardware> of(hardware)); .<Hardware> of(hardware));
Provider<TemplateOptions> optionsProvider = createMock(Provider.class); Provider<TemplateOptions> optionsProvider = createMock(Provider.class);
@ -131,12 +163,103 @@ public class TemplateBuilderImplTest {
TemplateBuilderImpl template = createTemplateBuilder(null, locations, images, hardwares, region, TemplateBuilderImpl template = createTemplateBuilder(null, locations, images, hardwares, region,
optionsProvider, templateBuilderProvider); optionsProvider, templateBuilderProvider);
template = builderCustomisation.apply(template);
assertEquals(template.resolveImage(hardware, images.get()), image2); assertEquals(template.resolveImage(hardware, images.get()), expectedBest);
verify(defaultTemplate, optionsProvider, templateBuilderProvider); verify(defaultTemplate, optionsProvider, templateBuilderProvider);
} }
public void testResolveImagesSimple() {
doTestResolveImages(Suppliers.<Set<? extends Image>> ofInstance(ImmutableSet.<Image> of(
image, image64bit)),
image64bit, Functions.<TemplateBuilderImpl>identity());
}
public void testResolveImagesPrefersNull() {
// preferring null has been the default behaviour; not sure if this is ideal
// (would make more sense to prefer nonNull) but don't change behaviour by default
doTestResolveImages(Suppliers.<Set<? extends Image>> ofInstance(ImmutableSet.<Image> of(
image, imageArchNull, image64bit)),
imageArchNull, Functions.<TemplateBuilderImpl>identity());
}
public void testResolveImagesCustomSorterPreferringNonNull() {
final Ordering<Image> sorterPreferringNonNullArch = new Ordering<Image>() {
@Override
public int compare(Image left, Image right) {
return ComparisonChain.start()
.compare(left.getOperatingSystem().getArch(), right.getOperatingSystem().getArch(),
Ordering.<String> natural().nullsFirst())
.compare(left, right, TemplateBuilderImpl.DEFAULT_IMAGE_ORDERING)
.result();
}
};
assertTrue(TemplateBuilderImpl.DEFAULT_IMAGE_ORDERING.compare(image64bit, imageArchNull) < 0, "wrong default image ordering");
assertTrue(sorterPreferringNonNullArch.compare(image64bit, imageArchNull) > 0, "wrong custom image ordering");
// preferring null has been the default behaviour;
// see comments in TemplateBuilderImpl.DEFAULT_IMAGE_ORDERING
doTestResolveImages(Suppliers.<Set<? extends Image>> ofInstance(ImmutableSet.<Image> of(
image, imageArchNull, image64bit)),
image64bit, new Function<TemplateBuilderImpl,TemplateBuilderImpl>() {
@Override
public TemplateBuilderImpl apply(TemplateBuilderImpl input) {
return input.imageChooser(input.imageChooserFromOrdering(sorterPreferringNonNullArch));
}
});
}
public void testResolveImagesPrefersImageBecauseNameIsLastAlphabetically() {
// preferring that which comes later alphabetically is the default behaviour;
// see comments in TemplateBuilderImpl.DEFAULT_IMAGE_ORDERING
doTestResolveImages(Suppliers.<Set<? extends Image>> ofInstance(ImmutableSet.<Image> of(
imageNameAlt, image)),
image, Functions.<TemplateBuilderImpl>identity());
}
public void testResolveImagesCustomSorterPreferringAltImage() {
doTestResolveImages(Suppliers.<Set<? extends Image>> ofInstance(ImmutableSet.<Image> of(
imageNameAlt, image, imageArchNull, image64bit)),
imageNameAlt, new Function<TemplateBuilderImpl,TemplateBuilderImpl>() {
@Override
public TemplateBuilderImpl apply(TemplateBuilderImpl input) {
return input.imageChooser(input.imageChooserFromOrdering(new Ordering<Image>() {
private int score(Image img) {
if (img.getName().contains("alternate")) return 10;
return 0;
}
@Override
public int compare(Image left, Image right) {
return score(left) - score(right);
}
}));
}
});
}
public void testResolveImagesCustomChooserPreferringAltImage() {
doTestResolveImages(Suppliers.<Set<? extends Image>> ofInstance(ImmutableSet.<Image> of(
imageNameAlt, image, imageArchNull, image64bit)),
imageNameAlt, new Function<TemplateBuilderImpl,TemplateBuilderImpl>() {
@Override
public TemplateBuilderImpl apply(TemplateBuilderImpl input) {
return input.imageChooser(new Function<Iterable<? extends Image>, Image>() {
@Override
public Image apply(Iterable<? extends Image> input) {
return Iterables.find(input, new Predicate<Image>() {
@Override
public boolean apply(Image input) {
return input.getName()!=null && input.getName().contains("alternate");
}
});
}
});
}
});
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Test @Test
public void testArchWins() { public void testArchWins() {
@ -146,7 +269,7 @@ public class TemplateBuilderImplTest {
Supplier<Set<? extends Location>> locations = Suppliers.<Set<? extends Location>> ofInstance(ImmutableSet Supplier<Set<? extends Location>> locations = Suppliers.<Set<? extends Location>> ofInstance(ImmutableSet
.<Location> of(region)); .<Location> of(region));
Supplier<Set<? extends Image>> images = Suppliers.<Set<? extends Image>> ofInstance(ImmutableSet.<Image> of( Supplier<Set<? extends Image>> images = Suppliers.<Set<? extends Image>> ofInstance(ImmutableSet.<Image> of(
image, image2)); image, image64bit));
Supplier<Set<? extends Hardware>> hardwares = Suppliers.<Set<? extends Hardware>> ofInstance(ImmutableSet Supplier<Set<? extends Hardware>> hardwares = Suppliers.<Set<? extends Hardware>> ofInstance(ImmutableSet
.<Hardware> of(hardware)); .<Hardware> of(hardware));
Provider<TemplateOptions> optionsProvider = createMock(Provider.class); Provider<TemplateOptions> optionsProvider = createMock(Provider.class);
@ -169,7 +292,7 @@ public class TemplateBuilderImplTest {
@Test @Test
public void testHardwareWithImageIdPredicateOnlyAcceptsImage() { public void testHardwareWithImageIdPredicateOnlyAcceptsImage() {
Hardware hardware = new HardwareBuilder().id("hardwareId").supportsImage(ImagePredicates.idEquals("us-east-1/imageId")) Hardware hardware = new HardwareBuilder().id("hardwareId").supportsImage(ImagePredicates.idEquals(getProviderFormatId("imageId")))
.build(); .build();
Supplier<Set<? extends Location>> locations = Suppliers.<Set<? extends Location>> ofInstance(ImmutableSet Supplier<Set<? extends Location>> locations = Suppliers.<Set<? extends Location>> ofInstance(ImmutableSet
@ -191,7 +314,7 @@ public class TemplateBuilderImplTest {
TemplateBuilderImpl template = createTemplateBuilder(null, locations, images, hardwares, region, TemplateBuilderImpl template = createTemplateBuilder(null, locations, images, hardwares, region,
optionsProvider, templateBuilderProvider); optionsProvider, templateBuilderProvider);
template.imageId("us-east-1/imageId").build(); template.imageId(getProviderFormatId("imageId")).build();
verify(defaultTemplate); verify(defaultTemplate);
verify(optionsProvider); verify(optionsProvider);
@ -202,7 +325,7 @@ public class TemplateBuilderImplTest {
@Test @Test
public void testHardwareWithImageIdPredicateOnlyAcceptsImageWhenLocationNull() { public void testHardwareWithImageIdPredicateOnlyAcceptsImageWhenLocationNull() {
Hardware hardware = new HardwareBuilder().id("hardwareId").supportsImage(ImagePredicates.idEquals("us-east-1/imageId")) Hardware hardware = new HardwareBuilder().id("hardwareId").supportsImage(ImagePredicates.idEquals(getProviderFormatId("imageId")))
.build(); .build();
Supplier<Set<? extends Location>> locations = Suppliers.<Set<? extends Location>> ofInstance(ImmutableSet Supplier<Set<? extends Location>> locations = Suppliers.<Set<? extends Location>> ofInstance(ImmutableSet
@ -222,7 +345,7 @@ public class TemplateBuilderImplTest {
TemplateBuilderImpl template = createTemplateBuilder(null, locations, images, hardwares, region, TemplateBuilderImpl template = createTemplateBuilder(null, locations, images, hardwares, region,
optionsProvider, templateBuilderProvider); optionsProvider, templateBuilderProvider);
template.imageId("us-east-1/imageId").build(); template.imageId(getProviderFormatId("imageId")).build();
verify(defaultTemplate, optionsProvider, templateBuilderProvider); verify(defaultTemplate, optionsProvider, templateBuilderProvider);
@ -252,7 +375,7 @@ public class TemplateBuilderImplTest {
TemplateBuilderImpl template = createTemplateBuilder(image, locations, images, hardwares, region, TemplateBuilderImpl template = createTemplateBuilder(image, locations, images, hardwares, region,
optionsProvider, templateBuilderProvider); optionsProvider, templateBuilderProvider);
try { try {
template.imageId("us-east-1/imageId").build(); template.imageId(getProviderFormatId("imageId")).build();
fail("Expected NoSuchElementException"); fail("Expected NoSuchElementException");
} catch (NoSuchElementException e) { } catch (NoSuchElementException e) {
// make sure message is succinct // make sure message is succinct
@ -789,8 +912,6 @@ public class TemplateBuilderImplTest {
} }
@Test @Test
public void testFromSpecWithLoginUser() { public void testFromSpecWithLoginUser() {