Skip to content

Commit b105adb

Browse files
apuigjeromepochat
andauthored
Deprecate boolean associatePublicIp and add AssociateIpStrategy enum for IP assignment control (#1148)
* Deprecate boolean `associatePublicIp` and add `AssociateIpStrategy` enum for IP assignment control JENKINS-75002 Currently, public IP assignment behavior is inconsistent: - `associatePublicIp=true`: - Creates a `NetworkInterface` - Sets `associatePublicIpAddress(true)` - Configures subnet and security groups on the interface - `associatePublicIp=false`: - **On-demand**: - Does *not* use a `NetworkInterface` - Configures subnet/security groups at the RunInstance level - Public IP assignment depends on the subnet's `MapPublicIpOnLaunch` setting (inherited) - **Spot**: - Uses a `NetworkInterface` - Sets `associatePublicIpAddress(false)`, enforcing *no* public IP (does *not* inherit) This prevents reliable enforcement of "no public IP" via AWS Service Control Policies. The current logic follows [PR 447](#447), which reverted [JENKINS-58578](https://issues.jenkins.io/browse/JENKINS-58578), losing the ability of `associatePublicIp=false` to always disable public IP assignment. - **Always create** the primary `NetworkInterface`. - Replace the boolean `associatePublicIp` with the `AssociateIpStrategy` enum: - `PUBLIC_IP`: Always assign a public IP (`associatePublicIpAddress=true`) - `PRIVATE_IP`: Never assign a public IP (`associatePublicIpAddress=false`) - `SUBNET`: Inherit the subnet's `MapPublicIpOnLaunch` (leave unset) - `DEFAULT`: Matches current behavior: `PRIVATE_IP` for spot, `SUBNET` for on-demand The `DEFAULT` option maintains backward compatibility and is used as the UI default. * permission check in AssociateIPStrategy requests * fix javadoc links * Use label 'Associate IP Strategy' instead of 'Associate IP' Co-authored-by: Jérôme Pochat <[email protected]> --------- Co-authored-by: Jérôme Pochat <[email protected]>
1 parent 95ca008 commit b105adb

18 files changed

+453
-120
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ def sshPortToConnectWith = '22'
315315
// store parameters
316316
def slaveTemplateUsEast1Parameters = [
317317
ami: 'ami-AAAAAAAA',
318-
associatePublicIp: false,
318+
associateIpStrategy: AssociateIpStrategy.valueOf('PRIVATE_IP'),
319319
spotConfig: null,
320320
connectBySSHProcess: false,
321321
connectUsingPublicIp: false,
@@ -445,7 +445,7 @@ SlaveTemplate slaveTemplateUsEast1 = new SlaveTemplate(
445445
slaveTemplateUsEast1Parameters.deleteRootOnTermination,
446446
slaveTemplateUsEast1Parameters.useEphemeralDevices,
447447
slaveTemplateUsEast1Parameters.launchTimeoutStr,
448-
slaveTemplateUsEast1Parameters.associatePublicIp,
448+
slaveTemplateUsEast1Parameters.associateIpStrategy,
449449
slaveTemplateUsEast1Parameters.customDeviceMapping,
450450
slaveTemplateUsEast1Parameters.connectBySSHProcess,
451451
slaveTemplateUsEast1Parameters.monitoring,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package hudson.plugins.ec2;
2+
3+
/**
4+
*
5+
* Strategy for associating a public IPv4 address with the instance’s primary network interface at launch.
6+
*
7+
* @see <a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_InstanceNetworkInterfaceSpecification.html">AWS Network Interface Specification</a>
8+
* @see <a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Subnet.html">AWS Subnet API</a>
9+
* */
10+
public enum AssociateIPStrategy {
11+
SUBNET("Inherit from Subnet"),
12+
PUBLIC_IP("Public IP"),
13+
PRIVATE_IP("Private IP"),
14+
DEFAULT("Default");
15+
16+
private final String displayText;
17+
18+
AssociateIPStrategy(String displayText) {
19+
this.displayText = displayText;
20+
}
21+
22+
public String getDisplayText() {
23+
return this.displayText;
24+
}
25+
26+
/**
27+
* For backwards compatibility.
28+
* @param associatePublicIp whether or not to use a public ip to establish a connection.
29+
* @return an {@link AssociateIPStrategy} based on provided parameters that keeps {@code associatePublicIp} behavior.
30+
*/
31+
public static AssociateIPStrategy backwardsCompatible(boolean associatePublicIp) {
32+
return associatePublicIp ? PUBLIC_IP : DEFAULT;
33+
}
34+
}

src/main/java/hudson/plugins/ec2/SlaveTemplate.java

Lines changed: 173 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ public class SlaveTemplate implements Describable<SlaveTemplate> {
217217

218218
public HostKeyVerificationStrategyEnum hostKeyVerificationStrategy;
219219

220-
public final boolean associatePublicIp;
220+
public AssociateIPStrategy associateIPStrategy;
221+
222+
@Deprecated
223+
public transient boolean associatePublicIp;
221224

222225
protected transient EC2Cloud parent;
223226

@@ -325,7 +328,7 @@ public SlaveTemplate(
325328
boolean deleteRootOnTermination,
326329
boolean useEphemeralDevices,
327330
String launchTimeoutStr,
328-
boolean associatePublicIp,
331+
AssociateIPStrategy associateIPStrategy,
329332
String customDeviceMapping,
330333
boolean connectBySSHProcess,
331334
boolean monitoring,
@@ -382,7 +385,6 @@ public SlaveTemplate(
382385
this.subnetId = subnetId;
383386
this.tags = tags;
384387
this.idleTerminationMinutes = idleTerminationMinutes;
385-
this.associatePublicIp = associatePublicIp;
386388
this.connectionStrategy = connectionStrategy == null ? ConnectionStrategy.PRIVATE_IP : connectionStrategy;
387389
this.useDedicatedTenancy = tenancy == Tenancy.Dedicated;
388390
this.connectBySSHProcess = connectBySSHProcess;
@@ -431,9 +433,106 @@ public SlaveTemplate(
431433
this.metadataHopsLimit =
432434
metadataHopsLimit != null ? metadataHopsLimit : EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT;
433435
this.enclaveEnabled = enclaveEnabled != null ? enclaveEnabled : EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED;
436+
this.associateIPStrategy = associateIPStrategy != null ? associateIPStrategy : AssociateIPStrategy.DEFAULT;
437+
434438
readResolve(); // initialize
435439
}
436440

441+
@Deprecated
442+
public SlaveTemplate(
443+
String ami,
444+
String zone,
445+
SpotConfiguration spotConfig,
446+
String securityGroups,
447+
String remoteFS,
448+
String type,
449+
boolean ebsOptimized,
450+
String labelString,
451+
Node.Mode mode,
452+
String description,
453+
String initScript,
454+
String tmpDir,
455+
String userData,
456+
String numExecutors,
457+
String remoteAdmin,
458+
AMITypeData amiType,
459+
String javaPath,
460+
String jvmopts,
461+
boolean stopOnTerminate,
462+
String subnetId,
463+
List<EC2Tag> tags,
464+
String idleTerminationMinutes,
465+
int minimumNumberOfInstances,
466+
int minimumNumberOfSpareInstances,
467+
String instanceCapStr,
468+
String iamInstanceProfile,
469+
boolean deleteRootOnTermination,
470+
boolean useEphemeralDevices,
471+
String launchTimeoutStr,
472+
boolean associatePublicIp,
473+
String customDeviceMapping,
474+
boolean connectBySSHProcess,
475+
boolean monitoring,
476+
boolean t2Unlimited,
477+
ConnectionStrategy connectionStrategy,
478+
int maxTotalUses,
479+
List<? extends NodeProperty<?>> nodeProperties,
480+
HostKeyVerificationStrategyEnum hostKeyVerificationStrategy,
481+
Tenancy tenancy,
482+
EbsEncryptRootVolume ebsEncryptRootVolume,
483+
Boolean metadataEndpointEnabled,
484+
Boolean metadataTokensRequired,
485+
Integer metadataHopsLimit,
486+
Boolean metadataSupported,
487+
Boolean enclaveEnabled) {
488+
this(
489+
ami,
490+
zone,
491+
spotConfig,
492+
securityGroups,
493+
remoteFS,
494+
InstanceType.fromValue(type.toString()).toString(),
495+
ebsOptimized,
496+
labelString,
497+
mode,
498+
description,
499+
initScript,
500+
tmpDir,
501+
userData,
502+
numExecutors,
503+
remoteAdmin,
504+
amiType,
505+
javaPath,
506+
jvmopts,
507+
stopOnTerminate,
508+
subnetId,
509+
tags,
510+
idleTerminationMinutes,
511+
minimumNumberOfInstances,
512+
minimumNumberOfSpareInstances,
513+
instanceCapStr,
514+
iamInstanceProfile,
515+
deleteRootOnTermination,
516+
useEphemeralDevices,
517+
launchTimeoutStr,
518+
AssociateIPStrategy.backwardsCompatible(associatePublicIp),
519+
customDeviceMapping,
520+
connectBySSHProcess,
521+
monitoring,
522+
t2Unlimited,
523+
connectionStrategy,
524+
maxTotalUses,
525+
nodeProperties,
526+
hostKeyVerificationStrategy,
527+
tenancy,
528+
ebsEncryptRootVolume,
529+
metadataEndpointEnabled,
530+
metadataTokensRequired,
531+
metadataHopsLimit,
532+
metadataSupported,
533+
enclaveEnabled);
534+
}
535+
437536
@Deprecated
438537
public SlaveTemplate(
439538
String ami,
@@ -1640,24 +1739,29 @@ public String getCurrentSubnetId() {
16401739
return currentSubnetId;
16411740
}
16421741

1742+
@Deprecated
16431743
public boolean getAssociatePublicIp() {
1644-
return associatePublicIp;
1744+
return AssociateIPStrategy.PUBLIC_IP == associateIPStrategy;
1745+
}
1746+
1747+
public AssociateIPStrategy getAssociateIPStrategy() {
1748+
return associateIPStrategy;
16451749
}
16461750

16471751
@Deprecated
16481752
@DataBoundSetter
16491753
public void setConnectUsingPublicIp(boolean connectUsingPublicIp) {
16501754
this.connectUsingPublicIp = connectUsingPublicIp;
16511755
this.connectionStrategy = ConnectionStrategy.backwardsCompatible(
1652-
this.usePrivateDnsName, this.connectUsingPublicIp, this.associatePublicIp);
1756+
this.usePrivateDnsName, this.connectUsingPublicIp, getAssociatePublicIp());
16531757
}
16541758

16551759
@Deprecated
16561760
@DataBoundSetter
16571761
public void setUsePrivateDnsName(boolean usePrivateDnsName) {
16581762
this.usePrivateDnsName = usePrivateDnsName;
16591763
this.connectionStrategy = ConnectionStrategy.backwardsCompatible(
1660-
this.usePrivateDnsName, this.connectUsingPublicIp, this.associatePublicIp);
1764+
this.usePrivateDnsName, this.connectUsingPublicIp, getAssociatePublicIp());
16611765
}
16621766

16631767
@Deprecated
@@ -2011,11 +2115,7 @@ HashMap<RunInstancesRequest, List<Filter>> makeRunInstancesRequestAndFilters(
20112115

20122116
InstanceNetworkInterfaceSpecification.Builder netBuilder = InstanceNetworkInterfaceSpecification.builder();
20132117
if (StringUtils.isNotBlank(subnetId)) {
2014-
if (getAssociatePublicIp()) {
2015-
netBuilder.subnetId(subnetId);
2016-
} else {
2017-
riRequestBuilder.subnetId(subnetId);
2018-
}
2118+
netBuilder.subnetId(subnetId);
20192119

20202120
diFilters.add(Filter.builder().name("subnet-id").values(subnetId).build());
20212121

@@ -2026,11 +2126,7 @@ HashMap<RunInstancesRequest, List<Filter>> makeRunInstancesRequestAndFilters(
20262126
List<String> groupIds = getEc2SecurityGroups(ec2);
20272127

20282128
if (!groupIds.isEmpty()) {
2029-
if (getAssociatePublicIp()) {
2030-
netBuilder.groups(groupIds);
2031-
} else {
2032-
riRequestBuilder.securityGroupIds(groupIds);
2033-
}
2129+
netBuilder.groups(groupIds);
20342130

20352131
diFilters.add(Filter.builder()
20362132
.name("instance.group-id")
@@ -2042,11 +2138,8 @@ HashMap<RunInstancesRequest, List<Filter>> makeRunInstancesRequestAndFilters(
20422138
List<String> groupIds = getSecurityGroupsBy("group-name", securityGroupSet, ec2).securityGroups().stream()
20432139
.map(SecurityGroup::groupId)
20442140
.collect(Collectors.toList());
2045-
if (getAssociatePublicIp()) {
2046-
netBuilder.groups(groupIds);
2047-
} else {
2048-
riRequestBuilder.securityGroups(securityGroupSet);
2049-
}
2141+
netBuilder.groups(groupIds);
2142+
20502143
if (!groupIds.isEmpty()) {
20512144
diFilters.add(Filter.builder()
20522145
.name("instance.group-id")
@@ -2055,13 +2148,21 @@ HashMap<RunInstancesRequest, List<Filter>> makeRunInstancesRequestAndFilters(
20552148
}
20562149
}
20572150

2058-
netBuilder.associatePublicIpAddress(getAssociatePublicIp());
2059-
netBuilder.deviceIndex(0);
2060-
2061-
if (getAssociatePublicIp()) {
2062-
riRequestBuilder.networkInterfaces(netBuilder.build());
2151+
switch (getAssociateIPStrategy()) {
2152+
case PUBLIC_IP:
2153+
netBuilder.associatePublicIpAddress(true);
2154+
break;
2155+
case PRIVATE_IP:
2156+
netBuilder.associatePublicIpAddress(false);
2157+
break;
2158+
case SUBNET:
2159+
case DEFAULT:
2160+
break;
20632161
}
20642162

2163+
netBuilder.deviceIndex(0);
2164+
riRequestBuilder.networkInterfaces(netBuilder.build());
2165+
20652166
HashSet<Tag> instTags = buildTags(EC2Cloud.EC2_SLAVE_TYPE_DEMAND);
20662167
for (Tag tag : instTags) {
20672168
diFilters.add(Filter.builder()
@@ -2505,7 +2606,18 @@ private List<EC2AbstractSlave> provisionSpot(Image image, int number, EnumSet<Pr
25052606
launchSpecificationBuilder.keyName(keyPair.getKeyPairInfo().keyName());
25062607
launchSpecificationBuilder.instanceType(type);
25072608

2508-
netBuilder.associatePublicIpAddress(getAssociatePublicIp());
2609+
switch (getAssociateIPStrategy()) {
2610+
case PUBLIC_IP:
2611+
netBuilder.associatePublicIpAddress(true);
2612+
break;
2613+
case PRIVATE_IP:
2614+
case DEFAULT:
2615+
netBuilder.associatePublicIpAddress(false);
2616+
break;
2617+
case SUBNET:
2618+
break;
2619+
}
2620+
25092621
netBuilder.deviceIndex(0);
25102622
launchSpecificationBuilder.networkInterfaces(netBuilder.build());
25112623

@@ -2889,10 +3001,14 @@ protected Object readResolve() {
28893001
type = InstanceTypeCompat.of(type).toString();
28903002
}
28913003

3004+
if (associateIPStrategy == null) {
3005+
associateIPStrategy = AssociateIPStrategy.backwardsCompatible(associatePublicIp);
3006+
}
3007+
28923008
// 1.43 new parameters
28933009
if (connectionStrategy == null) {
2894-
connectionStrategy =
2895-
ConnectionStrategy.backwardsCompatible(usePrivateDnsName, connectUsingPublicIp, associatePublicIp);
3010+
connectionStrategy = ConnectionStrategy.backwardsCompatible(
3011+
usePrivateDnsName, connectUsingPublicIp, AssociateIPStrategy.PUBLIC_IP == associateIPStrategy);
28963012
}
28973013

28983014
if (maxTotalUses == 0) {
@@ -3384,6 +3500,34 @@ public FormValidation doCheckConnectionStrategy(@QueryParameter String connectio
33843500
.orElse(FormValidation.error("Could not find selected connection strategy"));
33853501
}
33863502

3503+
@POST
3504+
public ListBoxModel doFillAssociateIPStrategyItems(@QueryParameter String associateIPStrategy) {
3505+
checkPermission(EC2Cloud.PROVISION);
3506+
return Stream.of(AssociateIPStrategy.values())
3507+
.map(v -> {
3508+
if (v.name().equals(associateIPStrategy)) {
3509+
return new ListBoxModel.Option(v.getDisplayText(), v.name(), true);
3510+
} else {
3511+
return new ListBoxModel.Option(v.getDisplayText(), v.name(), false);
3512+
}
3513+
})
3514+
.collect(Collectors.toCollection(ListBoxModel::new));
3515+
}
3516+
3517+
@POST
3518+
public FormValidation doCheckAssociateIPStrategy(@QueryParameter String associateIPStrategy) {
3519+
checkPermission(EC2Cloud.PROVISION);
3520+
return Stream.of(AssociateIPStrategy.values())
3521+
.filter(v -> v.name().equals(associateIPStrategy))
3522+
.findFirst()
3523+
.map(s -> FormValidation.ok())
3524+
.orElse(FormValidation.error("Could not find selected associate IP strategy"));
3525+
}
3526+
3527+
public String getDefaultAssociateIPStrategy() {
3528+
return AssociateIPStrategy.DEFAULT.name();
3529+
}
3530+
33873531
public String getDefaultHostKeyVerificationStrategy() {
33883532
// new templates default to the most secure strategy
33893533
return HostKeyVerificationStrategyEnum.CHECK_NEW_HARD.name();

src/main/resources/hudson/plugins/ec2/SlaveTemplate/config.jelly

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,8 @@ THE SOFTWARE.
205205
<f:textbox />
206206
</f:entry>
207207

208-
<f:entry title="${%Associate Public IP}" field="associatePublicIp">
209-
<f:checkbox />
208+
<f:entry title="${%Associate IP Strategy}" field="associateIPStrategy">
209+
<f:select default="${descriptor.getDefaultAssociateIPStrategy()}"/>
210210
</f:entry>
211211

212212
<f:entry title="${%Tenancy}" field="tenancy">
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<div>
2+
Strategy for associating a public IPv4 address with the instance’s primary network interface at launch.
3+
<ul>
4+
<li><strong>Public IP</strong>: Explicitly request a public IPv4 address.</li>
5+
<li><strong>Private IP</strong>: Explicitly prevent association of a public IPv4 address.</li>
6+
<li><strong>Inherit from Subnet</strong>: Choose this to delegate the decision to subnet configuration.
7+
Inherit the subnet’s behavior, allowing the subnet’s 'MapPublicIpOnLaunch' setting to decide whether a public IPv4 address is associated.</li>
8+
<li><strong>Default</strong>: Existing behavior is preserved: Spot instances use 'Private IP' (explicitly false), while on‑demand instances use 'Inherit from Subnet' (omit the field and use subnet setting).</li>
9+
</ul>
10+
</div>

0 commit comments

Comments
 (0)