From de0b5be3ed3bd2b4d7a8492b1dc7b9554e41d249 Mon Sep 17 00:00:00 2001 From: Lironrad <64735199+Lironrad@users.noreply.github.com> Date: Thu, 29 Dec 2022 14:08:16 +0200 Subject: [PATCH] Jenkins plugin instance type search (#27) Added capability to search for instances by instance types --- JenkinsWiki.adoc | 5 + .../spotinst/cloud/AwsSpotinstCloud.java | 13 +- .../cloud/SpotinstInstanceWeight.java | 174 +++++++++++++++--- .../AwsSpotinstCloudInstanceTypeMonitor.java | 83 +++++++++ .../AwsInstanceTypeSelectMethodEnum.java | 37 ++++ .../cloud/SpotinstInstanceWeight/config.jelly | 16 +- .../message.jelly | 15 ++ .../message.properties | 3 + 8 files changed, 316 insertions(+), 30 deletions(-) create mode 100644 src/main/java/hudson/plugins/spotinst/cloud/monitor/AwsSpotinstCloudInstanceTypeMonitor.java create mode 100644 src/main/java/hudson/plugins/spotinst/common/AwsInstanceTypeSelectMethodEnum.java create mode 100644 src/main/resources/hudson/plugins/spotinst/cloud/monitor/AwsSpotinstCloudInstanceTypeMonitor/message.jelly create mode 100644 src/main/resources/hudson/plugins/spotinst/cloud/monitor/AwsSpotinstCloudInstanceTypeMonitor/message.properties diff --git a/JenkinsWiki.adoc b/JenkinsWiki.adoc index 8e994761..9d4c93ad 100644 --- a/JenkinsWiki.adoc +++ b/JenkinsWiki.adoc @@ -40,6 +40,11 @@ termination" [SpotinstPlugin-Versionhistory] == Version history +[SpotinstPlugin-Version2.2.9(Dec29,2022)] +=== Version 2.2.9 (Dec 29, 2022) + +* Added Search Instance Type by Input feature + [SpotinstPlugin-Version2.2.8(Jul14,2022)] === Version 2.2.8 (Jul 14, 2022) diff --git a/src/main/java/hudson/plugins/spotinst/cloud/AwsSpotinstCloud.java b/src/main/java/hudson/plugins/spotinst/cloud/AwsSpotinstCloud.java index 8cbd91f8..387080ac 100644 --- a/src/main/java/hudson/plugins/spotinst/cloud/AwsSpotinstCloud.java +++ b/src/main/java/hudson/plugins/spotinst/cloud/AwsSpotinstCloud.java @@ -32,6 +32,7 @@ public class AwsSpotinstCloud extends BaseSpotinstCloud { private static final String CLOUD_URL = "aws/ec2"; protected Map executorsByInstanceType; private List executorsForTypes; + private List invalidInstanceTypes; //endregion //region Constructor @@ -370,13 +371,19 @@ private void addSpotinstSlave(AwsGroupInstance instance) { private void initExecutorsByInstanceType() { this.executorsByInstanceType = new HashMap<>(); + this.invalidInstanceTypes = new LinkedList<>(); if (this.executorsForTypes != null) { for (SpotinstInstanceWeight instance : this.executorsForTypes) { if (instance.getExecutors() != null) { Integer executors = instance.getExecutors(); - String type = instance.getAwsInstanceTypeFromAPI(); + String type = instance.getAwsInstanceTypeFromAPIInput(); this.executorsByInstanceType.put(type, executors); + + if(instance.getIsValid() == false){ + LOGGER.error(String.format("Invalid type \'%s\' in group \'%s\'", type, this.getGroupId())); + invalidInstanceTypes.add(type); + } } } } @@ -387,6 +394,10 @@ private void initExecutorsByInstanceType() { public List getExecutorsForTypes() { return executorsForTypes; } + + public List getInvalidInstanceTypes() { + return this.invalidInstanceTypes; + } //endregion //region Classes diff --git a/src/main/java/hudson/plugins/spotinst/cloud/SpotinstInstanceWeight.java b/src/main/java/hudson/plugins/spotinst/cloud/SpotinstInstanceWeight.java index 2d184d93..24f15c82 100644 --- a/src/main/java/hudson/plugins/spotinst/cloud/SpotinstInstanceWeight.java +++ b/src/main/java/hudson/plugins/spotinst/cloud/SpotinstInstanceWeight.java @@ -1,9 +1,11 @@ package hudson.plugins.spotinst.cloud; import hudson.Extension; +import hudson.model.AutoCompletionCandidates; import hudson.model.Describable; import hudson.model.Descriptor; import hudson.plugins.spotinst.common.AwsInstanceTypeEnum; +import hudson.plugins.spotinst.common.AwsInstanceTypeSelectMethodEnum; import hudson.plugins.spotinst.common.SpotAwsInstanceTypesHelper; import hudson.plugins.spotinst.common.SpotinstContext; import hudson.plugins.spotinst.model.aws.AwsInstanceType; @@ -12,8 +14,10 @@ import jenkins.model.Jenkins; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; import java.util.List; +import java.util.stream.Stream; import static hudson.plugins.spotinst.api.SpotinstApi.validateToken; @@ -22,10 +26,13 @@ */ public class SpotinstInstanceWeight implements Describable { //region Members - private Integer executors; - private String awsInstanceTypeFromAPI; + private Integer executors; + private String awsInstanceTypeFromAPI; + private String awsInstanceTypeFromAPISearch; + private AwsInstanceTypeSelectMethodEnum selectMethod; + private boolean isValid; //Deprecated - private AwsInstanceTypeEnum awsInstanceType; + private AwsInstanceTypeEnum awsInstanceType; //endregion //region Constructors @@ -47,6 +54,67 @@ public Descriptor getDescriptor() { return retVal; } + + @Override + public String toString() { + return "SpotinstInstanceWeight:{ " + "Pick: " + this.awsInstanceTypeFromAPI + ", " + "Search: " + + this.awsInstanceTypeFromAPISearch + ", " + "type: " + this.awsInstanceType + ", " + "executors: " + + this.executors + " }"; + + } + //endregion + + //region Methods + public String getAwsInstanceTypeFromAPIInput() { + String type; + AwsInstanceTypeSelectMethodEnum selectMethod = getSelectMethod(); + + if (selectMethod == AwsInstanceTypeSelectMethodEnum.SEARCH) { + type = getAwsInstanceTypeFromAPISearch(); + } + else { + type = getAwsInstanceTypeFromAPI(); + } + + return type; + } + //endregion + + //region Private Methods + private String getAwsInstanceTypeByName(String awsInstanceTypeFromAPIName) { + String retVal = null; + + if (awsInstanceTypeFromAPIName != null) { + + /* + If the user Previously chosen was a type that not exist in the hard coded list + and did not configure the token right, we will present the chosen type and set the default vCPU to 1 + The descriptor of this class will show a warning message will note the user that something is wrong, + and point to authentication fix before saving this configuration. + */ + List types = SpotAwsInstanceTypesHelper.getAllInstanceTypes(); + isValid = types.stream().anyMatch(i -> i.getInstanceType().equals(awsInstanceTypeFromAPIName)); + + if (isValid == false) { + if (getSelectMethod() != AwsInstanceTypeSelectMethodEnum.SEARCH) { + AwsInstanceType instanceType = new AwsInstanceType(); + instanceType.setInstanceType(awsInstanceTypeFromAPIName); + instanceType.setvCPU(1); + SpotinstContext.getInstance().getAwsInstanceTypes().add(instanceType); + } + } + + retVal = awsInstanceTypeFromAPIName; + + } + else { + if (awsInstanceType != null) { + retVal = awsInstanceType.getValue(); + } + } + + return retVal; + } //endregion //region Classes @@ -71,13 +139,36 @@ public ListBoxModel doFillAwsInstanceTypeFromAPIItems() { return retVal; } + public AutoCompletionCandidates doAutoCompleteAwsInstanceTypeFromAPISearch(@QueryParameter String value) { + AutoCompletionCandidates retVal = new AutoCompletionCandidates(); + List allAwsInstanceTypes = SpotAwsInstanceTypesHelper.getAllInstanceTypes(); + Stream allTypes = + allAwsInstanceTypes.stream().map(AwsInstanceType::getInstanceType); + Stream matchingTypes = allTypes.filter(type -> type.startsWith(value)); + matchingTypes.forEach(retVal::add); + + return retVal; + } + public FormValidation doCheckAwsInstanceTypeFromAPI() { + FormValidation retVal = CheckAccountIdAndToken(); + + return retVal; + } + + public FormValidation doCheckAwsInstanceTypeFromAPISearch() { + FormValidation retVal = CheckAccountIdAndToken(); + + return retVal; + } + + private FormValidation CheckAccountIdAndToken() { FormValidation retVal = null; String accountId = SpotinstContext.getInstance().getAccountId(); String token = SpotinstContext.getInstance().getSpotinstToken(); int isValid = validateToken(token, accountId); - Boolean isInstanceTypesListUpdate = SpotAwsInstanceTypesHelper.isInstanceTypesListUpdate(); + boolean isInstanceTypesListUpdate = SpotAwsInstanceTypesHelper.isInstanceTypesListUpdate(); if (isValid != 0 || isInstanceTypesListUpdate == false) { retVal = FormValidation.error( @@ -99,42 +190,73 @@ public AwsInstanceTypeEnum getAwsInstanceType() { return awsInstanceType; } + public String getAwsInstanceTypeFromAPI() { + String retVal; + + if (selectMethod != AwsInstanceTypeSelectMethodEnum.SEARCH) { + retVal = getAwsInstanceTypeByName(this.awsInstanceTypeFromAPI); + } + else { + retVal = this.awsInstanceTypeFromAPI; + } + + return retVal; + } + @DataBoundSetter public void setAwsInstanceTypeFromAPI(String awsInstanceTypeFromAPI) { this.awsInstanceTypeFromAPI = awsInstanceTypeFromAPI; + + if (selectMethod != AwsInstanceTypeSelectMethodEnum.SEARCH) { + this.awsInstanceTypeFromAPISearch = awsInstanceTypeFromAPI; + } } - public String getAwsInstanceTypeFromAPI() { - String retVal = null; + public String getAwsInstanceTypeFromAPISearch() { + String retVal; - if (this.awsInstanceTypeFromAPI != null) { + if (selectMethod == AwsInstanceTypeSelectMethodEnum.SEARCH) { + retVal = getAwsInstanceTypeByName(this.awsInstanceTypeFromAPISearch); + } + else { + retVal = this.awsInstanceTypeFromAPISearch; + } - /* - If the user Previously chosen was a type that not exist in the hard coded list - and did not configure the token right, we will present the chosen type and set the default vCPU to 1 - The descriptor of this class will show a warning message will note the user that something is wrong, - and point to authentication fix before saving this configuration. - */ - List types = SpotAwsInstanceTypesHelper.getAllInstanceTypes(); - boolean isTypeInList = types.stream().anyMatch(i -> i.getInstanceType().equals(this.awsInstanceTypeFromAPI)); + return retVal; + } - if (isTypeInList == false) { - AwsInstanceType instanceType = new AwsInstanceType(); - instanceType.setInstanceType(awsInstanceTypeFromAPI); - instanceType.setvCPU(1); - SpotinstContext.getInstance().getAwsInstanceTypes().add(instanceType); - } + @DataBoundSetter + public void setAwsInstanceTypeFromAPISearch(String awsInstanceTypeFromAPISearch) { + this.awsInstanceTypeFromAPISearch = awsInstanceTypeFromAPISearch; + + if (selectMethod == AwsInstanceTypeSelectMethodEnum.SEARCH) { + this.awsInstanceTypeFromAPI = awsInstanceTypeFromAPISearch; + } + } - retVal = awsInstanceTypeFromAPI; + public AwsInstanceTypeSelectMethodEnum getSelectMethod() { + AwsInstanceTypeSelectMethodEnum retVal = AwsInstanceTypeSelectMethodEnum.PICK; + if (selectMethod != null) { + retVal = selectMethod; + } + + return retVal; + } + + @DataBoundSetter + public void setSelectMethod(AwsInstanceTypeSelectMethodEnum selectMethod) { + + if (selectMethod == null) { + this.selectMethod = AwsInstanceTypeSelectMethodEnum.PICK; } else { - if(awsInstanceType != null){ - retVal = awsInstanceType.getValue(); - } + this.selectMethod = selectMethod; } + } - return retVal; + public boolean getIsValid() { + return this.isValid; } //endregion } diff --git a/src/main/java/hudson/plugins/spotinst/cloud/monitor/AwsSpotinstCloudInstanceTypeMonitor.java b/src/main/java/hudson/plugins/spotinst/cloud/monitor/AwsSpotinstCloudInstanceTypeMonitor.java new file mode 100644 index 00000000..180c70e7 --- /dev/null +++ b/src/main/java/hudson/plugins/spotinst/cloud/monitor/AwsSpotinstCloudInstanceTypeMonitor.java @@ -0,0 +1,83 @@ +package hudson.plugins.spotinst.cloud.monitor; + +import hudson.Extension; +import hudson.model.AdministrativeMonitor; +import hudson.plugins.spotinst.cloud.AwsSpotinstCloud; +import hudson.slaves.Cloud; +import jenkins.model.Jenkins; +import org.apache.commons.collections.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Extension +public class AwsSpotinstCloudInstanceTypeMonitor extends AdministrativeMonitor { + //region members + Map> invalidInstancesByGroupId; + //endregion + + //region Overrides + @Override + public boolean isActivated() { + boolean retVal; + initInvalidInstances(); + retVal = hasInvalidInstanceType(); + return retVal; + } + + @Override + public String getDisplayName() { + return "Aws Spotinst Cloud Instance Type Monitor"; + } + //endregion + + //region Methods + public boolean hasInvalidInstanceType() { + return invalidInstancesByGroupId.isEmpty() == false; + } + //endregion + + //region getters & setters + public String getInvalidInstancesByGroupId() { + Stream invalidInstancesForOutput = + invalidInstancesByGroupId.keySet().stream().map(this::generateAlertMessage); + String retVal = invalidInstancesForOutput.collect(Collectors.joining(", ")); + + return retVal; + } + //endregion + + //region private Methods + private void initInvalidInstances() { + invalidInstancesByGroupId = new HashMap<>(); + Jenkins jenkinsInstance = Jenkins.getInstance(); + List clouds = jenkinsInstance != null ? jenkinsInstance.clouds : new LinkedList<>(); + List awsClouds = clouds.stream().filter(cloud -> cloud instanceof AwsSpotinstCloud) + .map(awsCloud -> (AwsSpotinstCloud) awsCloud) + .collect(Collectors.toList()); + + awsClouds.forEach(awsCloud -> { + String elastigroupId = awsCloud.getGroupId(); + List invalidTypes = awsCloud.getInvalidInstanceTypes(); + + if (CollectionUtils.isEmpty(invalidTypes) == false) { + invalidInstancesByGroupId.put(elastigroupId, invalidTypes); + } + }); + } + + private String generateAlertMessage(String group) { + StringBuilder retVal = new StringBuilder(); + retVal.append('\'').append(group).append('\'').append(": ["); + + List InvalidInstancesByGroup = invalidInstancesByGroupId.get(group); + Stream InvalidInstancesForAlert = + InvalidInstancesByGroup.stream().map(invalidInstance -> '\'' + invalidInstance + '\''); + + String instances = InvalidInstancesForAlert.collect(Collectors.joining(", ")); + retVal.append(instances).append(']'); + return retVal.toString(); + } + //region +} diff --git a/src/main/java/hudson/plugins/spotinst/common/AwsInstanceTypeSelectMethodEnum.java b/src/main/java/hudson/plugins/spotinst/common/AwsInstanceTypeSelectMethodEnum.java new file mode 100644 index 00000000..c647d816 --- /dev/null +++ b/src/main/java/hudson/plugins/spotinst/common/AwsInstanceTypeSelectMethodEnum.java @@ -0,0 +1,37 @@ +package hudson.plugins.spotinst.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public enum AwsInstanceTypeSelectMethodEnum { + PICK("PICK"), + SEARCH("SEARCH"); + + private final String name; + + private static final Logger LOGGER = LoggerFactory.getLogger(AwsInstanceTypeSelectMethodEnum.class); + + AwsInstanceTypeSelectMethodEnum(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static AwsInstanceTypeSelectMethodEnum fromName(String name) { + AwsInstanceTypeSelectMethodEnum retVal = null; + for (AwsInstanceTypeSelectMethodEnum selectMethodEnum : AwsInstanceTypeSelectMethodEnum.values()) { + if (selectMethodEnum.name.equals(name)) { + retVal = selectMethodEnum; + break; + } + } + + if (retVal == null) { + LOGGER.error("Tried to create select method type enum for: " + name + ", but we don't support such type "); + } + + return retVal; + } +} diff --git a/src/main/resources/hudson/plugins/spotinst/cloud/SpotinstInstanceWeight/config.jelly b/src/main/resources/hudson/plugins/spotinst/cloud/SpotinstInstanceWeight/config.jelly index f342aade..12bd0dd5 100644 --- a/src/main/resources/hudson/plugins/spotinst/cloud/SpotinstInstanceWeight/config.jelly +++ b/src/main/resources/hudson/plugins/spotinst/cloud/SpotinstInstanceWeight/config.jelly @@ -1,8 +1,18 @@ - - - + + + + + + + + + + + diff --git a/src/main/resources/hudson/plugins/spotinst/cloud/monitor/AwsSpotinstCloudInstanceTypeMonitor/message.jelly b/src/main/resources/hudson/plugins/spotinst/cloud/monitor/AwsSpotinstCloudInstanceTypeMonitor/message.jelly new file mode 100644 index 00000000..031334d3 --- /dev/null +++ b/src/main/resources/hudson/plugins/spotinst/cloud/monitor/AwsSpotinstCloudInstanceTypeMonitor/message.jelly @@ -0,0 +1,15 @@ + + +
+ + ${%Title} +

+ + ${%InvalidInstanceType(it.invalidInstancesByGroupId)} +

+ + ${%Explanation(rootURL)} +

+ +

+
\ No newline at end of file diff --git a/src/main/resources/hudson/plugins/spotinst/cloud/monitor/AwsSpotinstCloudInstanceTypeMonitor/message.properties b/src/main/resources/hudson/plugins/spotinst/cloud/monitor/AwsSpotinstCloudInstanceTypeMonitor/message.properties new file mode 100644 index 00000000..d6f361f8 --- /dev/null +++ b/src/main/resources/hudson/plugins/spotinst/cloud/monitor/AwsSpotinstCloudInstanceTypeMonitor/message.properties @@ -0,0 +1,3 @@ +Title=Invalid AWS Instance types +InvalidInstanceType=Cloud has settings for invalid instance types for elastigroup Groups & Types: {0} +Explanation= Please use another Instance type or delete the types' configuration from the cloud configuration. \ No newline at end of file