diff --git a/src/main/java/com/dubture/jenkins/digitalocean/DigitalOcean.java b/src/main/java/com/dubture/jenkins/digitalocean/DigitalOcean.java index 6ad3cfcc..bf414053 100644 --- a/src/main/java/com/dubture/jenkins/digitalocean/DigitalOcean.java +++ b/src/main/java/com/dubture/jenkins/digitalocean/DigitalOcean.java @@ -3,6 +3,7 @@ * * Copyright (c) 2015 Rory Hunter (rory.hunter@blackpepper.co.uk) * 2016 Maxim Biro + * 2017, 2021 Harald Sitter * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -25,6 +26,7 @@ package com.dubture.jenkins.digitalocean; +import java.text.MessageFormat; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; @@ -78,6 +80,12 @@ static List getAvailableSizes(String authToken) throws DigitalOceanExcepti return availableSizes; } + static enum ImageFilter + { + ALLIMAGES, + USERIMAGES + } + /** * Fetches all available images. Unlike the other getAvailable* methods, this returns a map because the values * are sorted by a key composed of their OS distribution and version, which is useful for display purposes. Backup @@ -88,17 +96,24 @@ static List getAvailableSizes(String authToken) throws DigitalOceanExcepti * @throws DigitalOceanException * @throws RequestUnsuccessfulException */ - static SortedMap getAvailableImages(String authToken) throws DigitalOceanException, RequestUnsuccessfulException { + static SortedMap getAvailableImages(String authToken, ImageFilter filter) throws DigitalOceanException, RequestUnsuccessfulException { DigitalOceanClient client = new DigitalOceanClient(authToken); SortedMap availableImages = new TreeMap<>(ignoringCase()); - Images images; + Images images = null; int page = 0; do { page += 1; - images = client.getAvailableImages(page, Integer.MAX_VALUE); + switch (filter) { + case ALLIMAGES: + images = client.getAvailableImages(page, Integer.MAX_VALUE); + break; + case USERIMAGES: + images = client.getUserImages(page, Integer.MAX_VALUE); + break; + } for (Image image : images.getImages()) { String prefix = getPrefix(image); final String name = prefix + image.getDistribution() + " " + image.getName(); @@ -116,6 +131,14 @@ static SortedMap getAvailableImages(String authToken) throws Digit return availableImages; } + static SortedMap getAvailableImages(String authToken) throws DigitalOceanException, RequestUnsuccessfulException { + return getAvailableImages(authToken, ImageFilter.ALLIMAGES); + } + + static SortedMap getAvailableUserImages(String authToken) throws DigitalOceanException, RequestUnsuccessfulException { + return getAvailableImages(authToken, ImageFilter.USERIMAGES); + } + private static String getPrefix(Image image) { if (image.getType() == ImageType.BACKUP) { @@ -252,6 +275,30 @@ static List getDroplets(String authToken) throws DigitalOceanException, return availableDroplets; } + static Image getMatchingNamedImage(String authToken, String imageName) throws DigitalOceanException, RequestUnsuccessfulException { + List matchingImages = new ArrayList(); + + final SortedMap images = getAvailableUserImages(authToken); + for (Image image : images.values()) { + if (imageName.equals(image.getName())) { + matchingImages.add(image); + } + } + + Collections.sort(matchingImages, new Comparator() { + @Override + public int compare(Image left, Image right) { + return left.getCreatedDate().compareTo(right.getCreatedDate()); + } + }); + + if (matchingImages.size() < 1) { + throw new RuntimeException(MessageFormat.format("Failed to resolve image name '{0}'", imageName)); + } + + return matchingImages.get(0); + } + /** * Fetches information for the specified droplet. * @param authToken the API authentication token to use @@ -265,8 +312,19 @@ static Droplet getDroplet(String authToken, Integer dropletId) throws DigitalOce return new DigitalOceanClient(authToken).getDropletInfo(dropletId); } - static Image newImage(String idOrSlug) { - Image image = new Image(idOrSlug); + static Image newImage(String authToken, String idOrSlugOrName, Boolean imageByName) throws DigitalOceanException, RequestUnsuccessfulException { + if (imageByName) { + return getMatchingNamedImage(authToken, idOrSlugOrName); + } + + Image image; + try { + image = new Image(Integer.parseInt(idOrSlugOrName)); + } + catch (NumberFormatException e) { + image = new Image(idOrSlugOrName); + } + return image; } diff --git a/src/main/java/com/dubture/jenkins/digitalocean/SlaveTemplate.java b/src/main/java/com/dubture/jenkins/digitalocean/SlaveTemplate.java index 35c2beb2..51c54b01 100644 --- a/src/main/java/com/dubture/jenkins/digitalocean/SlaveTemplate.java +++ b/src/main/java/com/dubture/jenkins/digitalocean/SlaveTemplate.java @@ -3,7 +3,7 @@ * * Copyright (c) 2014 robert.gruendler@dubture.com * 2016 Maxim Biro - * 2017 Harald Sitter + * 2017, 2021 Harald Sitter * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -93,6 +93,8 @@ public class SlaveTemplate implements Describable { private final Boolean labellessJobsAllowed; + private final Boolean imageByName; + /** * The Image to be used for the droplet. */ @@ -155,17 +157,19 @@ public class SlaveTemplate implements Describable { * @param tags the droplet tags * @param userData user data for DigitalOcean to apply when building the agent * @param initScript setup script to configure the agent + * @param imageByName whether to resolve the image by name rather than id */ @DataBoundConstructor public SlaveTemplate(String name, String imageId, String sizeId, String regionId, String username, String workspacePath, Integer sshPort, Boolean setupPrivateNetworking, String idleTerminationInMinutes, String numExecutors, String labelString, Boolean labellessJobsAllowed, String instanceCap, Boolean installMonitoring, String tags, - String userData, String initScript) { + String userData, String initScript, Boolean imageByName) { LOGGER.log(Level.INFO, "Creating SlaveTemplate with imageId = {0}, sizeId = {1}, regionId = {2}", new Object[] { imageId, sizeId, regionId}); this.name = name; + this.imageByName = imageByName; this.imageId = imageId; this.sizeId = sizeId; this.regionId = regionId; @@ -257,7 +261,7 @@ public Slave provision(ProvisioningActivity.Id provisioningId, droplet.setName(dropletName); droplet.setSize(sizeId); droplet.setRegion(new Region(regionId)); - droplet.setImage(DigitalOcean.newImage(imageId)); + droplet.setImage(DigitalOcean.newImage(authToken, imageId, imageByName)); droplet.setKeys(Arrays.asList(new Key(sshKeyId))); droplet.setInstallMonitoring(installMonitoringAgent); droplet.setEnablePrivateNetworking( @@ -442,22 +446,21 @@ public ListBoxModel doFillSizeIdItems(@RelativePath("..") @QueryParameter String return model; } - public ListBoxModel doFillImageIdItems(@RelativePath("..") @QueryParameter String authTokenCredentialId) throws Exception { - + public ListBoxModel doFillImageIdItems(@RelativePath("..") @QueryParameter String authTokenCredentialId, @QueryParameter Boolean imageByName) throws Exception { ListBoxModel model = new ListBoxModel(); - String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId); + final String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId); if (StringUtils.isBlank(authToken)) { return model; } - SortedMap availableImages = DigitalOcean.getAvailableImages(DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId)); + final SortedMap availableImages = imageByName ? DigitalOcean.getAvailableUserImages(authToken) : DigitalOcean.getAvailableImages(authToken); for (Map.Entry entry : availableImages.entrySet()) { final Image image = entry.getValue(); // For non-snapshots, use the image ID instead of the slug (which isn't available anyway) // so that we can build images based upon backups. - final String value = DigitalOcean.getImageIdentifier(image); + final String value = imageByName ? image.getName() : DigitalOcean.getImageIdentifier(image); model.add(entry.getKey(), value); } @@ -515,6 +518,10 @@ public Set getLabelSet() { return labelSet; } + public Boolean getImageByName() { + return imageByName; + } + public String getImageId() { return imageId; } diff --git a/src/main/resources/com/dubture/jenkins/digitalocean/SlaveTemplate/config.jelly b/src/main/resources/com/dubture/jenkins/digitalocean/SlaveTemplate/config.jelly index fbdc51e9..f6fe263a 100644 --- a/src/main/resources/com/dubture/jenkins/digitalocean/SlaveTemplate/config.jelly +++ b/src/main/resources/com/dubture/jenkins/digitalocean/SlaveTemplate/config.jelly @@ -3,6 +3,7 @@ ~ ~ Copyright (c) 2014 robert.gruendler@dubture.com ~ 2016 Maxim Biro + ~ 2021 Harald Sitter ~ ~ Permission is hereby granted, free of charge, to any person obtaining a copy ~ of this software and associated documentation files (the "Software"), to deal @@ -31,6 +32,10 @@ + + + + @@ -54,7 +59,7 @@ - + diff --git a/src/main/resources/com/dubture/jenkins/digitalocean/SlaveTemplate/help-imageByName.html b/src/main/resources/com/dubture/jenkins/digitalocean/SlaveTemplate/help-imageByName.html new file mode 100644 index 00000000..21167667 --- /dev/null +++ b/src/main/resources/com/dubture/jenkins/digitalocean/SlaveTemplate/help-imageByName.html @@ -0,0 +1,31 @@ + + +
+ Always resolves the actually used image through its name. Usually images are either resolved through their slug + or their unique id. The only reason to opt into resolution by name is when you recreate snapshots using the same + name to force updates into the cloud nodes. If multiple images are present the newest is used. + +

This option may only be used with user images, not base images provided by DigitalOcean!

+