From a8f0bb32987c8cee9e1405b09b69ce310957a6a2 Mon Sep 17 00:00:00 2001 From: Lironrad <64735199+Lironrad@users.noreply.github.com> Date: Thu, 14 Jul 2022 11:40:17 +0300 Subject: [PATCH 1/4] [src] new feature: single task nodes --- JenkinsWiki.adoc | 5 + .../spotinst/cloud/AwsSpotinstCloud.java | 4 +- .../spotinst/cloud/BaseSpotinstCloud.java | 114 ++++++++++++------ .../cloud/SpotGlobalExecutorOverride.java | 4 + .../spotinst/slave/SpotinstComputer.java | 46 ++++++- .../slave/SpotinstNonLocalizable.java | 12 ++ .../slave/SpotinstSingleTaskOfflineCause.java | 9 ++ .../cloud/BaseSpotinstCloud/config.jelly | 9 +- 8 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 src/main/java/hudson/plugins/spotinst/slave/SpotinstNonLocalizable.java create mode 100644 src/main/java/hudson/plugins/spotinst/slave/SpotinstSingleTaskOfflineCause.java diff --git a/JenkinsWiki.adoc b/JenkinsWiki.adoc index 1bbf5cfa..8e994761 100644 --- a/JenkinsWiki.adoc +++ b/JenkinsWiki.adoc @@ -40,6 +40,11 @@ termination" [SpotinstPlugin-Versionhistory] == Version history +[SpotinstPlugin-Version2.2.8(Jul14,2022)] +=== Version 2.2.8 (Jul 14, 2022) + +* Added 'Single Task Nodes' feature + [SpotinstPlugin-Version2.2.7(Jan17,2022)] === Version 2.2.7 (Jan 17, 2022) diff --git a/src/main/java/hudson/plugins/spotinst/cloud/AwsSpotinstCloud.java b/src/main/java/hudson/plugins/spotinst/cloud/AwsSpotinstCloud.java index b82bf72e..8cbd91f8 100644 --- a/src/main/java/hudson/plugins/spotinst/cloud/AwsSpotinstCloud.java +++ b/src/main/java/hudson/plugins/spotinst/cloud/AwsSpotinstCloud.java @@ -195,7 +195,7 @@ protected Integer getDefaultExecutorsNumber(String instanceType) { //region Private Methods @Override - protected Integer getNumOfExecutors(String instanceType) { + protected int getOverridedNumberOfExecutors(String instanceType) { Integer retVal; if (executorsByInstanceType == null) { @@ -207,7 +207,7 @@ protected Integer getNumOfExecutors(String instanceType) { LOGGER.info(String.format("We have a weight definition for this type of %s", retVal)); } else { - retVal = super.getNumOfExecutors(instanceType); + retVal = NO_OVERRIDED_NUM_OF_EXECUTORS; } return retVal; diff --git a/src/main/java/hudson/plugins/spotinst/cloud/BaseSpotinstCloud.java b/src/main/java/hudson/plugins/spotinst/cloud/BaseSpotinstCloud.java index e02e70c2..4071b270 100644 --- a/src/main/java/hudson/plugins/spotinst/cloud/BaseSpotinstCloud.java +++ b/src/main/java/hudson/plugins/spotinst/cloud/BaseSpotinstCloud.java @@ -1,5 +1,6 @@ package hudson.plugins.spotinst.cloud; +import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.DescriptorExtensionList; import hudson.model.*; import hudson.model.labels.LabelAtom; @@ -14,6 +15,7 @@ import hudson.tools.ToolLocationNodeProperty; import jenkins.model.Jenkins; import org.apache.commons.lang.BooleanUtils; +import org.kohsuke.stapler.DataBoundSetter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,25 +32,27 @@ public abstract class BaseSpotinstCloud extends Cloud { //region Members private static final Logger LOGGER = LoggerFactory.getLogger(BaseSpotinstCloud.class); - protected String accountId; - protected String groupId; - protected Map pendingInstances; - protected Map slaveInstancesDetailsByInstanceId; - private String labelString; - private String idleTerminationMinutes; - private String workspaceDir; - private Set labelSet; - private SlaveUsageEnum usage; - private String tunnel; - private String vmargs; - private EnvironmentVariablesNodeProperty environmentVariables; - private ToolLocationNodeProperty toolLocations; - private Boolean shouldUseWebsocket; - private Boolean shouldRetriggerBuilds; - private ComputerConnector computerConnector; - private ConnectionMethodEnum connectionMethod; - private Boolean shouldUsePrivateIp; - private SpotGlobalExecutorOverride globalExecutorOverride; + protected static final int NO_OVERRIDED_NUM_OF_EXECUTORS = -1; + protected String accountId; + protected String groupId; + protected Map pendingInstances; + protected Map slaveInstancesDetailsByInstanceId; + private String labelString; + private String idleTerminationMinutes; + private String workspaceDir; + private Set labelSet; + private SlaveUsageEnum usage; + private String tunnel; + private String vmargs; + private EnvironmentVariablesNodeProperty environmentVariables; + private ToolLocationNodeProperty toolLocations; + private Boolean shouldUseWebsocket; + private Boolean shouldRetriggerBuilds; + private Boolean isSingleTaskNodesEnabled; + private ComputerConnector computerConnector; + private ConnectionMethodEnum connectionMethod; + private Boolean shouldUsePrivateIp; + private SpotGlobalExecutorOverride globalExecutorOverride; //endregion //region Constructor @@ -106,7 +110,6 @@ public BaseSpotinstCloud(String groupId, String labelString, String idleTerminat else { this.globalExecutorOverride = new SpotGlobalExecutorOverride(false, 1); } - } //endregion @@ -585,28 +588,43 @@ protected PendingExecutorsCounts getPendingExecutors(ProvisionRequest request) { protected Integer getNumOfExecutors(String instanceType) { Integer retVal; + boolean isSingleTaskNodesEnabled = getIsSingleTaskNodesEnabled(); - Integer globalOverrideExecutorsNumber = getExecutorsFromGlobalOverride(); - - if (globalOverrideExecutorsNumber != null) { - LOGGER.debug(String.format("Overriding executors for instance type %s to be %s", instanceType, - globalOverrideExecutorsNumber)); - retVal = globalOverrideExecutorsNumber; + if (isSingleTaskNodesEnabled) { + retVal = 1; } else { - Integer defaultNumberOfExecutors = getDefaultExecutorsNumber(instanceType); + int overridedNumOfExecutors = getOverridedNumberOfExecutors(instanceType); + boolean isNumOfExecutorsOverrided = overridedNumOfExecutors != NO_OVERRIDED_NUM_OF_EXECUTORS; - if (defaultNumberOfExecutors != null) { - retVal = defaultNumberOfExecutors; + if(isNumOfExecutorsOverrided){ + retVal = overridedNumOfExecutors; } else { - retVal = 1; - String warningMsg = String.format( - "Failed to determine # of executors for instance type %s, defaulting to %s executor(s). Group ID: %s", - instanceType, retVal, this.getGroupId()); - LOGGER.warn(warningMsg); + Integer globalOverrideExecutorsNumber = getExecutorsFromGlobalOverride(); + + if (globalOverrideExecutorsNumber != null) { + LOGGER.debug(String.format("Overriding executors for instance type %s to be %s", instanceType, + globalOverrideExecutorsNumber)); + retVal = globalOverrideExecutorsNumber; + } + else { + Integer defaultNumberOfExecutors = getDefaultExecutorsNumber(instanceType); + + if (defaultNumberOfExecutors != null) { + retVal = defaultNumberOfExecutors; + } + else { + retVal = 1; + String warningMsg = String.format( + "Failed to determine # of executors for instance type %s, defaulting to %s executor(s). Group ID: %s", + instanceType, retVal, this.getGroupId()); + LOGGER.warn(warningMsg); + } + } } + } LOGGER.debug(String.format("instance type executors number was set to %s", retVal)); @@ -614,6 +632,10 @@ protected Integer getNumOfExecutors(String instanceType) { return retVal; } + protected int getOverridedNumberOfExecutors(String instanceType) { + return NO_OVERRIDED_NUM_OF_EXECUTORS; + } + protected Integer getPendingThreshold() { return Constants.PENDING_INSTANCE_TIMEOUT_IN_MINUTES; } @@ -734,6 +756,30 @@ public SpotGlobalExecutorOverride getGlobalExecutorOverride() { public void setGlobalExecutorOverride(SpotGlobalExecutorOverride globalExecutorOverride) { this.globalExecutorOverride = globalExecutorOverride; } + + + public Boolean getIsSingleTaskNodesEnabled() { + if (this.isSingleTaskNodesEnabled == null) { + return false; + } + else { + return isSingleTaskNodesEnabled; + } + } + + @DataBoundSetter + public void setIsSingleTaskNodesEnabled(Boolean isSingleTaskNodesEnabled) { + this.isSingleTaskNodesEnabled = isSingleTaskNodesEnabled; + + // if enabled, enable and override GlobalExecutorOverride to 1 + // better clarity to user, avoid race conditions + boolean shouldDisableGlobalExecutors = isSingleTaskNodesEnabled != null && isSingleTaskNodesEnabled && this.globalExecutorOverride != null; + if (shouldDisableGlobalExecutors) { + this.globalExecutorOverride.setIsEnabled(false); + } + } + + //endregion //region Abstract Methods diff --git a/src/main/java/hudson/plugins/spotinst/cloud/SpotGlobalExecutorOverride.java b/src/main/java/hudson/plugins/spotinst/cloud/SpotGlobalExecutorOverride.java index bfc0e0aa..82411ad1 100644 --- a/src/main/java/hudson/plugins/spotinst/cloud/SpotGlobalExecutorOverride.java +++ b/src/main/java/hudson/plugins/spotinst/cloud/SpotGlobalExecutorOverride.java @@ -56,5 +56,9 @@ public Integer getExecutors() { public boolean getIsEnabled() { return isEnabled; } + + public void setIsEnabled(boolean isEnabled) { + this.isEnabled = isEnabled; + } //endregion } diff --git a/src/main/java/hudson/plugins/spotinst/slave/SpotinstComputer.java b/src/main/java/hudson/plugins/spotinst/slave/SpotinstComputer.java index f40fa93d..37cb2ee2 100644 --- a/src/main/java/hudson/plugins/spotinst/slave/SpotinstComputer.java +++ b/src/main/java/hudson/plugins/spotinst/slave/SpotinstComputer.java @@ -1,11 +1,16 @@ package hudson.plugins.spotinst.slave; +import hudson.model.Executor; import hudson.model.Node; +import hudson.model.Queue; +import hudson.plugins.spotinst.cloud.BaseSpotinstCloud; import hudson.slaves.SlaveComputer; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.interceptor.RequirePOST; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; @@ -15,7 +20,46 @@ public class SpotinstComputer extends SlaveComputer { //region Members - private long launchTime; + private static final Logger LOGGER = LoggerFactory.getLogger(SpotinstComputer.class); + private long launchTime; + //endregion + + //region overrides + // better than JobListener because in complicated pipelines Jenkins is the initial Node and + // we know the Node is a SpotinstNode (I'm not using that word). + @Override + public void taskAccepted(Executor executor, Queue.Task task) { + super.taskAccepted(executor, task); + SpotinstSlave spotinstNode = this.getNode(); + + if (spotinstNode != null) { + BaseSpotinstCloud spotinstCloud = spotinstNode.getSpotinstCloud(); + + if (spotinstCloud != null) { + if (spotinstCloud.getIsSingleTaskNodesEnabled()) { + String msg = String.format( + "Node %s has accepted a job and 'Single Task Nodes' setting on Cloud %s is on. Node will not accept any more jobs.", + spotinstNode.getNodeName(), spotinstCloud.getDisplayName()); + LOGGER.info(msg); + this.setAcceptingTasks(false); + // I see much better responsiveness from Jenkins in case of setting offline, both have same effect + // should also be decided with UI because each combo looks different in the node list. + SpotinstNonLocalizable spotinstNonLocalizable = new SpotinstNonLocalizable(msg); + SpotinstSingleTaskOfflineCause spotinstSingleTaskOfflineCause = new SpotinstSingleTaskOfflineCause(spotinstNonLocalizable); + this.setTemporarilyOffline(true,spotinstSingleTaskOfflineCause); + } + } + else { + LOGGER.error(String.format( + "Node %s has accepted a job but can't determine 'Single Task Nodes' setting because SpotinstNode's SpotinstCloud appears to be null.", + spotinstNode.getNodeName())); + } + } else { + LOGGER.error(String.format( + "Executor of Node %s has accepted a job but can't determine 'Single Task Nodes' setting because SpotinstNode is null.", executor.getOwner().getName())); + } + } + //endregion //region Constructor diff --git a/src/main/java/hudson/plugins/spotinst/slave/SpotinstNonLocalizable.java b/src/main/java/hudson/plugins/spotinst/slave/SpotinstNonLocalizable.java new file mode 100644 index 00000000..18908409 --- /dev/null +++ b/src/main/java/hudson/plugins/spotinst/slave/SpotinstNonLocalizable.java @@ -0,0 +1,12 @@ +package hudson.plugins.spotinst.slave; + +import jenkins.util.NonLocalizable; + +import java.util.Locale; + +class SpotinstNonLocalizable extends NonLocalizable { + + public SpotinstNonLocalizable(String nonLocalizable) { + super(nonLocalizable); + } +} diff --git a/src/main/java/hudson/plugins/spotinst/slave/SpotinstSingleTaskOfflineCause.java b/src/main/java/hudson/plugins/spotinst/slave/SpotinstSingleTaskOfflineCause.java new file mode 100644 index 00000000..96243fe3 --- /dev/null +++ b/src/main/java/hudson/plugins/spotinst/slave/SpotinstSingleTaskOfflineCause.java @@ -0,0 +1,9 @@ +package hudson.plugins.spotinst.slave; + +import hudson.slaves.OfflineCause; + +public class SpotinstSingleTaskOfflineCause extends OfflineCause.SimpleOfflineCause { + public SpotinstSingleTaskOfflineCause(SpotinstNonLocalizable nonLocalizable) { + super(nonLocalizable); + } +} diff --git a/src/main/resources/hudson/plugins/spotinst/cloud/BaseSpotinstCloud/config.jelly b/src/main/resources/hudson/plugins/spotinst/cloud/BaseSpotinstCloud/config.jelly index ed5536ae..e14eb586 100644 --- a/src/main/resources/hudson/plugins/spotinst/cloud/BaseSpotinstCloud/config.jelly +++ b/src/main/resources/hudson/plugins/spotinst/cloud/BaseSpotinstCloud/config.jelly @@ -116,9 +116,16 @@ - + + + + Each node will perform at most one job and get terminated after "Idle minutes before termination" (see setting above). + Enabling this setting effectively means: override the Default executor count setting and ignore any instance-weight overrides if those are set. + + + 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 2/4] 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 From a5537bcae272ff30246cc75863975ec03e1c1ac3 Mon Sep 17 00:00:00 2001 From: sitay93 <116492420+sitay93@users.noreply.github.com> Date: Thu, 29 Dec 2022 14:14:15 +0200 Subject: [PATCH 3/4] InstanceType_by_user_input JenkinsWiki.adoc (#28) --- JenkinsWiki.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JenkinsWiki.adoc b/JenkinsWiki.adoc index 9d4c93ad..d6c02b28 100644 --- a/JenkinsWiki.adoc +++ b/JenkinsWiki.adoc @@ -43,7 +43,7 @@ termination" [SpotinstPlugin-Version2.2.9(Dec29,2022)] === Version 2.2.9 (Dec 29, 2022) -* Added Search Instance Type by Input feature +* Added capability to search for instances by instance types [SpotinstPlugin-Version2.2.8(Jul14,2022)] === Version 2.2.8 (Jul 14, 2022) From bb33a63d385535efc258254c5c91e49001d4d93b Mon Sep 17 00:00:00 2001 From: sitay Date: Tue, 10 Jan 2023 12:30:34 +0200 Subject: [PATCH 4/4] release upgrade: mvn incremental enable --- .mvn/extensions.xml | 7 +++++++ .mvn/maven.config | 2 ++ pom.xml | 13 ++++++++----- 3 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 .mvn/extensions.xml create mode 100644 .mvn/maven.config diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml new file mode 100644 index 00000000..9ac2968b --- /dev/null +++ b/.mvn/extensions.xml @@ -0,0 +1,7 @@ + + + io.jenkins.tools.incrementals + git-changelist-maven-extension + 1.4 + + diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 00000000..2a0299c4 --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1,2 @@ +-Pconsume-incrementals +-Pmight-produce-incrementals diff --git a/pom.xml b/pom.xml index 8a1f54d1..bf6fd640 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ spotinst - 2.2.10-SNAPSHOT + ${revision}${changelist} hpi Spotinst plugin @@ -33,13 +33,16 @@ - scm:git:git://github.com/jenkinsci/spotinst-plugin.git - scm:git:git@github.com:jenkinsci/spotinst-plugin.git - https://github.com/jenkinsci/spotinst-plugin - HEAD + scm:git:git://github.com/${gitHubRepo}.git + scm:git:git@github.com:${gitHubRepo}.git + https://github.com/${gitHubRepo} + ${scmTag} + 2.2.10 + -SNAPSHOT + jenkinsci/spotinst-plugin 2.222.4 8