Skip to content

Commit 1b8ba9d

Browse files
- MaintenanceLinkTest issues are resolved, completely.
- New: `CloudUuidAction` - a minimal action class just for containing the duplicate cloud's UUID. - `CloudUuidStore` - helper for dealing with duplicate clouds. - New `targetKey` format for duplicate clouds - CLOUD:cloud-name:cloud-uuid. While non duplicate clouds' format remains as is. - Saving maintenance-config.xml UUID directory if the cloud is found to have duplicates. - Fixed deletion bugs in `MaintenanceLink`. Signed-off-by: Pratham Vaghela <prathamcomeslast@gmail.com>
1 parent d1f5237 commit 1b8ba9d

File tree

14 files changed

+420
-76
lines changed

14 files changed

+420
-76
lines changed

src/main/java/com/sap/prd/jenkins/plugins/agent_maintenance/CloudMaintenanceActionFactory.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import hudson.slaves.Cloud;
77
import java.util.Collection;
88
import java.util.Collections;
9+
import java.util.logging.Level;
10+
import java.util.logging.Logger;
911
import jenkins.model.TransientActionFactory;
1012

1113
/**
@@ -14,13 +16,21 @@
1416
@Extension
1517
public class CloudMaintenanceActionFactory extends TransientActionFactory<Cloud> {
1618

19+
private static final Logger LOGGER = Logger.getLogger(CloudMaintenanceActionFactory.class.getName());
20+
1721
@NonNull
1822
@Override
1923
public Collection<? extends Action> createFor(@NonNull Cloud target) {
20-
MaintenanceTarget mt = new MaintenanceTarget(MaintenanceTarget.TargetType.CLOUD, target.name);
21-
MaintenanceAction action = new MaintenanceAction(mt);
24+
try {
25+
String uuid = CloudUuidStore.getInstance().getOrCreateUuid(target);
26+
MaintenanceTarget mt = new MaintenanceTarget(MaintenanceTarget.TargetType.CLOUD, target.name, uuid);
27+
MaintenanceAction action = new MaintenanceAction(mt);
2228

23-
return Collections.singletonList(action);
29+
return Collections.singletonList(action);
30+
} catch (Exception e) {
31+
LOGGER.log(Level.WARNING, "Failed to resolve UUID for cloud: " + target.name, e);
32+
return Collections.emptyList();
33+
}
2434
}
2535

2636
@Override

src/main/java/com/sap/prd/jenkins/plugins/agent_maintenance/CloudMaintenanceProvisioningListener.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ public class CloudMaintenanceProvisioningListener extends CloudProvisioningListe
1717
@Override
1818
public CauseOfBlockage canProvision(Cloud cloud, Cloud.CloudState state, int numExecutors) {
1919
try {
20-
MaintenanceTarget target = new MaintenanceTarget(MaintenanceTarget.TargetType.CLOUD, cloud.name);
20+
String uuid = CloudUuidStore.getInstance().getUuidIfPresent(cloud);
21+
MaintenanceTarget target = new MaintenanceTarget(MaintenanceTarget.TargetType.CLOUD, cloud.name, uuid);
22+
2123
LOGGER.log(Level.FINER, "Checking for Maintenance Window for cloud {0}", cloud.name);
2224

2325
// Triggers automatic cleanup of expired windows.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.sap.prd.jenkins.plugins.agent_maintenance;
2+
3+
import hudson.model.Action;
4+
5+
/**
6+
* Persistent action that stores UUID for duplicate clouds.
7+
* This is a purely technical action --- not shown in UI.
8+
*/
9+
public class CloudUuidAction implements Action {
10+
11+
private final String uuid;
12+
13+
public CloudUuidAction(String uuid) {
14+
this.uuid = uuid;
15+
}
16+
17+
public String getUuid() {
18+
return uuid;
19+
}
20+
21+
@Override
22+
public String getIconFileName() {
23+
return null;
24+
}
25+
26+
@Override
27+
public String getDisplayName() {
28+
return null;
29+
}
30+
31+
@Override
32+
public String getUrlName() {
33+
return null;
34+
}
35+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package com.sap.prd.jenkins.plugins.agent_maintenance;
2+
3+
import hudson.model.Action;
4+
import hudson.slaves.Cloud;
5+
import java.io.IOException;
6+
import java.util.Objects;
7+
import java.util.UUID;
8+
import java.util.logging.Logger;
9+
import jenkins.model.Jenkins;
10+
11+
/**
12+
* Utility class to store and retrieve UUIDs for duplicate clouds.
13+
*/
14+
public final class CloudUuidStore {
15+
16+
private static final CloudUuidStore INSTANCE = new CloudUuidStore();
17+
private static final Logger LOGGER = Logger.getLogger(CloudUuidStore.class.getName());
18+
19+
private CloudUuidStore() {}
20+
21+
public static CloudUuidStore getInstance() {
22+
return INSTANCE;
23+
}
24+
25+
public String getOrCreateUuid(Cloud cloud) {
26+
String name = cloud.name;
27+
CloudUuidAction persistentAction = getPersistentAction(cloud);
28+
if (!hasDuplicates(name)) {
29+
if (persistentAction != null) {
30+
// Remove persisted action when duplicacy no longer exists
31+
cloud.getActions().remove(persistentAction);
32+
saveConfig();
33+
}
34+
return null;
35+
}
36+
37+
if (persistentAction != null) {
38+
return persistentAction.getUuid();
39+
}
40+
41+
String uuid = UUID.randomUUID().toString();
42+
CloudUuidAction newAction = new CloudUuidAction(uuid);
43+
cloud.getActions().add(newAction);
44+
saveConfig();
45+
46+
return uuid;
47+
}
48+
49+
public String getUuidIfPresent(Cloud cloud) {
50+
CloudUuidAction action = getPersistentAction(cloud);
51+
if (action != null) {
52+
return action.getUuid();
53+
}
54+
return null;
55+
}
56+
57+
/**
58+
* Removes the persisted CloudUuidAction from a cloud if present.
59+
* Used when a cloud no longer needs UUID tracking -> no more duplicates.
60+
*
61+
* @param cloud The cloud
62+
* @return true if action was removed, false if not present
63+
*/
64+
public boolean removeUuidIfPresent(Cloud cloud) {
65+
CloudUuidAction action = getPersistentAction(cloud);
66+
if (action != null) {
67+
cloud.getActions().remove(action);
68+
saveConfig();
69+
return true;
70+
}
71+
return false;
72+
}
73+
74+
/**
75+
* Gets the CloudUuidAction.
76+
*
77+
* @param cloud The cloud
78+
* @return Persistent CloudUuidAction if present, null otherwise
79+
*/
80+
private CloudUuidAction getPersistentAction(Cloud cloud) {
81+
var actions = cloud.getActions();
82+
for (Action a : actions) {
83+
if (a instanceof CloudUuidAction cua) {
84+
return cua;
85+
}
86+
}
87+
return null;
88+
}
89+
90+
private void saveConfig() {
91+
try {
92+
Jenkins.get().save();
93+
} catch (IOException e) {
94+
LOGGER.warning("Failed to save configuration for cloud UUID.");
95+
}
96+
}
97+
98+
public Cloud getCloudByTarget(MaintenanceTarget target) {
99+
if (target.getType() != MaintenanceTarget.TargetType.CLOUD) {
100+
throw new IllegalArgumentException("Target type must be CLOUD");
101+
}
102+
103+
String name = target.getName();
104+
String uuid = target.getUuid();
105+
Jenkins j = Jenkins.get();
106+
if (uuid == null) {
107+
return j.getCloud(name);
108+
}
109+
110+
for (Cloud cloud : j.clouds) {
111+
if (name.equals(cloud.name) && uuid.equals(getUuidIfPresent(cloud))) {
112+
return cloud;
113+
}
114+
}
115+
116+
LOGGER.warning("Could not find cloud with name " + name + " and UUID " + uuid);
117+
return null;
118+
}
119+
120+
/**
121+
* Counts the number of clouds with the given name.
122+
*
123+
* @param cloudName Name of the cloud
124+
* @return Number of clouds with the given name
125+
*/
126+
public int cloudDuplicates(String cloudName) {
127+
Jenkins j = Jenkins.get();
128+
return (int) j.clouds.stream()
129+
.filter(c -> Objects.equals(c.name, cloudName))
130+
.count();
131+
}
132+
133+
/**
134+
* Checks if there are duplicate clouds.
135+
*/
136+
public boolean hasDuplicates(String cloudName) {
137+
return cloudDuplicates(cloudName) > 1;
138+
}
139+
}

src/main/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceAction.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public class MaintenanceAction implements Action {
4646
@Restricted(NoExternalUse.class)
4747
public static final Permission[] CONFIGURE_AND_DISCONNECT = new Permission[]{Computer.DISCONNECT, Computer.CONFIGURE};
4848

49+
/**
50+
* Creates MaintenanceAction. Including UUID for clouds.
51+
*/
4952
public MaintenanceAction(MaintenanceTarget target) {
5053
this.target = target;
5154
}
@@ -159,10 +162,10 @@ public boolean isEnabled() {
159162
* @return <code>Cloud</code> instance of the maintenance action.
160163
*/
161164
public Cloud getCloud() {
162-
if (isCloud()) {
163-
return Jenkins.get().getCloud(target.getName());
165+
if (!isCloud()) {
166+
return null;
164167
}
165-
return null;
168+
return CloudUuidStore.getInstance().getCloudByTarget(target);
166169
}
167170

168171
/**
@@ -535,6 +538,11 @@ public void doIndex(StaplerRequest2 req, StaplerResponse2 rsp)
535538
}
536539
c.checkAnyPermission(Computer.EXTENDED_READ, Computer.CONFIGURE, Computer.DISCONNECT);
537540
} else {
541+
Cloud cloud = getCloud();
542+
if (cloud == null) {
543+
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
544+
return;
545+
}
538546
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
539547
}
540548

src/main/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceHelper.java

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class MaintenanceHelper {
3434

3535
private static final MaintenanceHelper INSTANCE = new MaintenanceHelper();
3636

37+
private static final CloudUuidStore CLOUD_UUID_STORE = CloudUuidStore.getInstance();
38+
3739
private final Map<String, MaintenanceDefinitions> cache = new ConcurrentHashMap<>();
3840

3941
private MaintenanceHelper() {
@@ -80,16 +82,17 @@ private boolean isValidUuid(String id) {
8082
public boolean isValidTarget(String targetKey) throws IOException {
8183
MaintenanceTarget target = MaintenanceTarget.fromKey(targetKey);
8284
String name = target.getName();
83-
85+
String uuid = target.getUuid();
86+
Jenkins j = Jenkins.get();
8487
return switch (target.getType()) {
85-
case AGENT -> Jenkins.get().getComputer(name) != null;
88+
case AGENT -> j.getComputer(name) != null;
8689
case CLOUD -> {
87-
try {
88-
yield Jenkins.get().getCloud(name) != null;
89-
} catch (Exception e) {
90-
yield Jenkins.get().clouds != null
91-
&& Jenkins.get().clouds.stream().anyMatch(c -> c.name.equals(name));
90+
if (uuid == null) {
91+
yield j.getCloud(name) != null;
9292
}
93+
yield j.clouds.stream()
94+
.anyMatch(c -> c.name.equals(name)
95+
&& uuid.equals(CLOUD_UUID_STORE.getUuidIfPresent(c)));
9396
}
9497
};
9598
}
@@ -398,24 +401,33 @@ public void saveMaintenanceWindows(String targetKey, MaintenanceDefinitions md)
398401
private XmlFile getMaintenanceWindowsFile(String targetKey) throws IOException {
399402
MaintenanceTarget target = MaintenanceTarget.fromKey(targetKey);
400403
String name = target.getName();
404+
String uuid = target.getUuid();
401405

402406
File baseDir = getTargetDirectory(target.getType());
403407

408+
if (MaintenanceTarget.TargetType.CLOUD.equals(target.getType())
409+
&& uuid != null) {
410+
File cloudDir = new File(baseDir, name);
411+
File uuidDir = new File(cloudDir, uuid);
412+
if (!uuidDir.mkdirs() && !uuidDir.exists()) {
413+
throw new IOException("Failed to create " + uuid + " directory: " + uuidDir);
414+
}
415+
return new XmlFile(new File(uuidDir, "maintenance-windows.xml"));
416+
}
417+
404418
return new XmlFile(new File(new File(baseDir, name), "maintenance-windows.xml"));
405419
}
406420

407-
private File getTargetDirectory(MaintenanceTarget.TargetType target) throws IOException {
421+
public File getTargetDirectory(MaintenanceTarget.TargetType target) throws IOException {
408422
// jenkins.model.Nodes#getNodesDirectory() is private, so we have to duplicate
409423
// it here.
410424
String type = switch (target) {
411425
case AGENT -> "nodes";
412426
case CLOUD -> "clouds";
413427
};
414428
File targetDir = new File(Jenkins.get().getRootDir(), type);
415-
if (!targetDir.exists()) {
416-
if (!targetDir.mkdirs() && !targetDir.exists()) {
417-
throw new IOException("Failed to create " + type + " directory: " + targetDir);
418-
}
429+
if (!targetDir.mkdirs() && !targetDir.exists()) {
430+
throw new IOException("Failed to create " + type + " directory: " + targetDir);
419431
} else if (!targetDir.isDirectory()) {
420432
throw new IOException(targetDir + " is not a directory");
421433
}
@@ -433,21 +445,24 @@ public void deleteAgent(String targetKey) {
433445
* @param newName new name of the agent
434446
*/
435447
public void renameAgent(String oldName, String newName) {
436-
MaintenanceDefinitions md = cache.get(oldName);
448+
MaintenanceTarget oldTarget = new MaintenanceTarget(MaintenanceTarget.TargetType.AGENT, oldName);
449+
MaintenanceTarget newTarget = new MaintenanceTarget(MaintenanceTarget.TargetType.AGENT, newName);
450+
MaintenanceDefinitions md = cache.get(oldTarget.toKey());
437451
if (md != null) {
438452
LOGGER.log(Level.FINEST, "Persisting existing maintenance windows after agent rename");
439-
cache.remove(oldName);
440-
cache.put(newName, md);
453+
cache.remove(oldTarget.toKey());
454+
cache.put(newTarget.toKey(), md);
441455
try {
442-
saveMaintenanceWindows(newName, md);
456+
saveMaintenanceWindows(newTarget.toKey(), md);
443457
} catch (IOException e) {
444458
LOGGER.log(Level.WARNING, "Failed to persists agent maintenance windows after agent rename {0}", newName);
445459
}
446460
}
447461
}
448462

449463
public void createAgent(String nodeName) {
450-
cache.put(nodeName, new MaintenanceDefinitions(new TreeSet<>(), new HashSet<>()));
464+
MaintenanceTarget target = new MaintenanceTarget(MaintenanceTarget.TargetType.AGENT, nodeName);
465+
cache.put(target.toKey(), new MaintenanceDefinitions(new TreeSet<>(), new HashSet<>()));
451466
}
452467

453468
@Restricted(NoExternalUse.class)
@@ -491,6 +506,7 @@ public boolean injectRetentionStrategy(Computer c) {
491506
public boolean removeRetentionStrategy(Computer c) {
492507
if (c instanceof SlaveComputer computer) {
493508
String computerName = computer.getName();
509+
MaintenanceTarget target = new MaintenanceTarget(MaintenanceTarget.TargetType.AGENT, computerName);
494510
@SuppressWarnings("unchecked")
495511
RetentionStrategy<SlaveComputer> strategy = computer.getRetentionStrategy();
496512
if (strategy instanceof AgentMaintenanceRetentionStrategy maintenanceStrategy) {
@@ -499,8 +515,8 @@ public boolean removeRetentionStrategy(Computer c) {
499515
node.setRetentionStrategy(maintenanceStrategy.getRegularRetentionStrategy());
500516
try {
501517
node.save();
502-
deleteAgent(computerName);
503-
XmlFile maintenanceFile = getMaintenanceWindowsFile(computerName);
518+
deleteAgent(target.toKey());
519+
XmlFile maintenanceFile = getMaintenanceWindowsFile(target.toKey());
504520
if (maintenanceFile.exists()) {
505521
maintenanceFile.delete();
506522
}

0 commit comments

Comments
 (0)