├── src ├── main │ ├── resources │ │ ├── lib │ │ │ └── permissions │ │ │ │ ├── taglib │ │ │ │ ├── hasAnyPermission.jelly │ │ │ │ └── hasNoPermission.jelly │ │ ├── META-INF │ │ │ └── hudson.remoting.ClassFilter │ │ ├── com │ │ │ └── sap │ │ │ │ └── prd │ │ │ │ └── jenkins │ │ │ │ └── plugins │ │ │ │ └── agent_maintenance │ │ │ │ ├── MaintenanceLink │ │ │ │ ├── index.properties │ │ │ │ ├── index_de.properties │ │ │ │ └── index.jelly │ │ │ │ ├── MaintenanceOfflineCause │ │ │ │ ├── cause.properties │ │ │ │ ├── cause_de.properties │ │ │ │ └── cause.jelly │ │ │ │ ├── flatpickr.css │ │ │ │ ├── MaintenanceAction │ │ │ │ ├── index.properties │ │ │ │ ├── config_de.properties │ │ │ │ ├── index_de.properties │ │ │ │ ├── config.jelly │ │ │ │ └── index.jelly │ │ │ │ ├── Messages_de.properties │ │ │ │ ├── MaintenanceWindow │ │ │ │ ├── config_de.properties │ │ │ │ ├── help-startTime_de.properties │ │ │ │ ├── config.properties │ │ │ │ ├── help-endTime.jelly │ │ │ │ ├── help-startTime.jelly │ │ │ │ ├── help-endTime_de.properties │ │ │ │ ├── help-endTime.properties │ │ │ │ ├── help-startTime.properties │ │ │ │ └── config.jelly │ │ │ │ ├── MaintenanceConfiguration │ │ │ │ ├── config_de.properties │ │ │ │ ├── config.jelly │ │ │ │ ├── help-injectRetentionstrategy.html │ │ │ │ └── help-injectRetentionstrategy_de.html │ │ │ │ ├── RecurringMaintenanceWindow │ │ │ │ ├── config_de.properties │ │ │ │ ├── config.properties │ │ │ │ ├── help-startTimeSpec.jelly │ │ │ │ ├── help-startTimeSpec.properties │ │ │ │ └── config.jelly │ │ │ │ ├── AgentMaintenanceRetentionStrategy │ │ │ │ ├── config_de.properties │ │ │ │ ├── help.html │ │ │ │ └── config.jelly │ │ │ │ ├── flatpickr.js │ │ │ │ ├── Messages.properties │ │ │ │ ├── agent-maintenance.css │ │ │ │ └── agent-maintenance.js │ │ ├── index.jelly │ │ └── images │ │ │ └── symbols │ │ │ └── maintenance.svg │ ├── webapp │ │ └── help │ │ │ ├── help-reason.html │ │ │ ├── help-reason_de.html │ │ │ ├── help-maintenanceList.html │ │ │ ├── help-maintenanceList_de.html │ │ │ ├── help-keepUpWhenActive.html │ │ │ ├── help-disable.html │ │ │ ├── help-enable.html │ │ │ ├── help-keepUpWhenActive_de.html │ │ │ ├── help-disable_de.html │ │ │ ├── help-enable_de.html │ │ │ ├── help-label.html │ │ │ ├── help-label_de.html │ │ │ ├── help-takeOnline.html │ │ │ ├── help-takeOnline_de.html │ │ │ ├── help-duration.html │ │ │ ├── help-maxWaitMinutes.html │ │ │ └── help-maxWaitMinutes_de.html │ └── java │ │ └── com │ │ └── sap │ │ └── prd │ │ └── jenkins │ │ └── plugins │ │ └── agent_maintenance │ │ ├── MaintenanceInterruption.java │ │ ├── MaintenanceActionFactory.java │ │ ├── MaintenanceDefinitions.java │ │ ├── MaintenanceOfflineCause.java │ │ ├── MaintenanceNodeListener.java │ │ ├── MaintenanceConfiguration.java │ │ ├── AgentMaintenanceRetentionStrategy.java │ │ ├── MaintenanceLink.java │ │ ├── RecurringMaintenanceWindow.java │ │ ├── MaintenanceWindow.java │ │ └── MaintenanceAction.java └── test │ ├── resources │ └── com │ │ └── sap │ │ └── prd │ │ └── jenkins │ │ └── plugins │ │ └── agent_maintenance │ │ └── MigrationTest │ │ ├── nodes │ │ └── agent │ │ │ ├── maintenance-windows.xml │ │ │ └── config.xml │ │ └── config.xml │ └── java │ └── com │ └── sap │ └── prd │ └── jenkins │ └── plugins │ └── agent_maintenance │ ├── RecurringTest.java │ ├── MigrationTest.java │ ├── MaintenanceHelperTest.java │ ├── BaseIntegrationTest.java │ ├── MaintenanceWindowTest.java │ ├── MaintenanceOfflineCauseTest.java │ ├── BasePermissionChecks.java │ ├── MaintenanceConfigurationTest.java │ ├── PermissionSetup.java │ ├── MaintenanceLinkTest.java │ ├── MaintenanceActionTest.java │ ├── MaintenanceNodeListenerTest.java │ ├── AgentMaintenanceRetentionStrategyTest.java │ └── IntegrationTest.java ├── .github ├── release-drafter.yml ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── cd.yaml │ ├── jenkins-security-scan.yml │ └── codeql.yml ├── .mvn ├── maven.config └── extensions.xml ├── .gitignore ├── docs └── images │ └── configure.PNG ├── Jenkinsfile ├── .build-config └── checkstyle-suppressions.xml ├── CONTRIBUTING.md ├── .reuse └── dep5 ├── README.md ├── pom.xml ├── LICENSES └── Apache-2.0.txt └── LICENSE /src/main/resources/lib/permissions/taglib: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /src/main/webapp/help/help-reason.html: -------------------------------------------------------------------------------- 1 | The reason for the maintenance. -------------------------------------------------------------------------------- /src/main/webapp/help/help-reason_de.html: -------------------------------------------------------------------------------- 1 | Der Grund für die Wartung. -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/agent-maintenance-plugin-developers 2 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/hudson.remoting.ClassFilter: -------------------------------------------------------------------------------- 1 | java.time.LocalDateTime -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | .checkstyle 3 | .classpath 4 | .project 5 | .settings 6 | work 7 | keystore 8 | .idea 9 | -------------------------------------------------------------------------------- /docs/images/configure.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/agent-maintenance-plugin/main/docs/images/configure.PNG -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin( 2 | useContainerAgent: true, 3 | forkCount: '0.5C', 4 | configurations: [ 5 | [platform: 'linux', jdk: 17] 6 | ]) 7 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceLink/index.properties: -------------------------------------------------------------------------------- 1 | deleteMaintenanceOf=Delete maintenance window for agent 2 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceOfflineCause/cause.properties: -------------------------------------------------------------------------------- 1 | maintenanceover=The maintenance window was not found. Probably it has just ended. -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/flatpickr.css: -------------------------------------------------------------------------------- 1 | .am__flatpickr { 2 | display: flex; 3 | gap: 5px; 4 | } 5 | 6 | .am__flatpickr > input { 7 | width: 150px; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/webapp/help/help-maintenanceList.html: -------------------------------------------------------------------------------- 1 |
2 | A list of maintenance windows for this agent.
3 | A maintenance window will be automatically removed from the list once it is over. 4 |
-------------------------------------------------------------------------------- /src/main/webapp/help/help-maintenanceList_de.html: -------------------------------------------------------------------------------- 1 |
2 | Eine Liste der Wartungsfenster für diesen Agenten.
3 | Ein Wartungsfenster wird nach Ablauf automatisch aus der Liste entfernt. 4 |
-------------------------------------------------------------------------------- /src/main/webapp/help/help-keepUpWhenActive.html: -------------------------------------------------------------------------------- 1 | If checked and this agent is scheduled to be taken offline but there are builds in progress, Jenkins will wait for those builds to complete before taking this agent offline.
2 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceAction/index.properties: -------------------------------------------------------------------------------- 1 | missingPermission=Maintenance Availability is currently not enabled for this agent. Computer.CONFIGURE permission is required to enable it. -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/Messages_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/agent-maintenance-plugin/main/src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/Messages_de.properties -------------------------------------------------------------------------------- /src/main/webapp/help/help-disable.html: -------------------------------------------------------------------------------- 1 |
2 | By clicking the Disable button all existing maintenance windows for this agent will be deleted.
3 | The currently defined Regular Availability will become the agents new availability. 4 |
-------------------------------------------------------------------------------- /src/main/webapp/help/help-enable.html: -------------------------------------------------------------------------------- 1 |
2 | By clicking the Enable button the Agent Maintenance Availability will be enabled for this agent.
3 | The currently defined Availability will become the agents new Regular Availability. 4 |
-------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceLink/index_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/agent-maintenance-plugin/main/src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceLink/index_de.properties -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: / 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceAction/config_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/agent-maintenance-plugin/main/src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceAction/config_de.properties -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceAction/index_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/agent-maintenance-plugin/main/src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceAction/index_de.properties -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceOfflineCause/cause_de.properties: -------------------------------------------------------------------------------- 1 | maintenanceover=Das Wartungsfenster wurde nicht gefunden. Wahrscheinlich ist es gerade zu Ende gegangen. 2 | Start\ Time=Startzeit 3 | End\ Time=Endzeit 4 | Reason=Grund -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceWindow/config_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/agent-maintenance-plugin/main/src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceWindow/config_de.properties -------------------------------------------------------------------------------- /src/main/webapp/help/help-keepUpWhenActive_de.html: -------------------------------------------------------------------------------- 1 | Wenn diese Option aktiviert ist und dieser Agent offline geschaltet werden soll, aber Builds ausgeführt werden, wartet Jenkins, 2 | bis diese Builds abgeschlossen sind, bevor dieser Agent offline geschaltet wird.
3 | -------------------------------------------------------------------------------- /src/main/webapp/help/help-disable_de.html: -------------------------------------------------------------------------------- 1 |
2 | Durch betätigen des Ausschalten Kopfes werden alle existierenden Wartungsfenster dieses Agenten gelöscht.
3 | Die momentan definierte Reguläre Verfügbarkeit wird die neue Verfügbarkeit des Agenten. 4 |
-------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceConfiguration/config_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/agent-maintenance-plugin/main/src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceConfiguration/config_de.properties -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceWindow/help-startTime_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/agent-maintenance-plugin/main/src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceWindow/help-startTime_de.properties -------------------------------------------------------------------------------- /src/main/webapp/help/help-enable_de.html: -------------------------------------------------------------------------------- 1 |
2 | Durch betätigen des Einschalten Knopfes wird die Agentenwartungsverfügbarkeit für diesen Agenten eingeschaltet.
3 | Die momenan definierte Verfügbarkeit wird die neue Reguläre Verfügbarkeit des Agenten. 4 |
-------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/RecurringMaintenanceWindow/config_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/agent-maintenance-plugin/main/src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/RecurringMaintenanceWindow/config_de.properties -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/AgentMaintenanceRetentionStrategy/config_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/agent-maintenance-plugin/main/src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/AgentMaintenanceRetentionStrategy/config_de.properties -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/RecurringMaintenanceWindow/config.properties: -------------------------------------------------------------------------------- 1 | Reason=Reason 2 | keepUpWhenActive=Keep agent online while jobs are running 3 | maxWaitMinutes=Max waiting time in minutes for builds to finish 4 | takeOnline=Start agent automatically after maintenance -------------------------------------------------------------------------------- /src/main/webapp/help/help-label.html: -------------------------------------------------------------------------------- 1 |
2 | A label expression selecting the agents for which to apply the maintenance window.
3 | The maintenance window will only be applied to those agents that have agent maintenance as availability configured and 4 | for which the user has configure permissions. 5 |
-------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | This plugin allows to take agents offline for one or more dedicated time windows on specific dates, e.g. due to a hardware maintenance.
4 | It is possible to define other agent availabilities, e.g. to take agent offline on a schedule. 5 |
-------------------------------------------------------------------------------- /src/main/webapp/help/help-label_de.html: -------------------------------------------------------------------------------- 1 |
2 | Ein Label-Ausdruck, der die Agenten auswählt, für die das Wartungsfenster angewendet werden soll.
3 | Das Wartungsfenster wird nur auf die Agenten angewendet, für die Agentenwartung als Verfügbarkeit konfiguriert ist und 4 | für die der Benutzer Konfigurationsberechtigungen hat. 5 |
-------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceWindow/config.properties: -------------------------------------------------------------------------------- 1 | endTime= End Time 2 | startTime= Start Time 3 | Reason=Reason 4 | keepUpWhenActive=Keep agent online while jobs are running 5 | maxWaitMinutes=Max waiting time in minutes for builds to finish 6 | takeOnline=Start agent automatically after maintenance -------------------------------------------------------------------------------- /src/main/webapp/help/help-takeOnline.html: -------------------------------------------------------------------------------- 1 | Automatically take the agent online according to its default availability after the maintenance is finished.
2 | If this option is not checked the agent will stay disconnected until it is manually launched (or Jenkins is restarted). 3 | During the maintenance window a manual launch is not possible.
-------------------------------------------------------------------------------- /src/main/webapp/help/help-takeOnline_de.html: -------------------------------------------------------------------------------- 1 | Nimmt den Agenten nach Abschluss der Wartung gemäß seiner Standardverfügbarkeit automatisch online.
2 | Wenn diese Option nicht aktiviert ist, bleibt der Agent getrennt, bis er manuell gestartet wird (oder Jenkins neu gestartet wird). 3 | Während des Wartungsfensters ist ein manueller Start nicht möglich. -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceWindow/help-endTime.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | ${%endTime(currentTimeZone.getID())} 6 |
7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceWindow/help-startTime.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | ${%startTime(currentTimeZone.getID())} 6 |
7 |
-------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceWindow/help-endTime_de.properties: -------------------------------------------------------------------------------- 1 | endTime=Datum und Uhrzeit zu der das Wartungsfenster enden soll in der Zeitzone der Jenkins Controller JVM (aktuell {0}).
\ 2 | Im Format yyyy-MM-dd HH:mm
\ 3 | Das Wartungsfenster wird automatisch entfernt, wenn die Wartung endet. 4 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceWindow/help-endTime.properties: -------------------------------------------------------------------------------- 1 | endTime=The date and time when the maintenance ends in the time zone of the Jenkins controller JVM (currently {0}).
\ 2 | Formatted as yyyy-MM-dd HH:mm
\ 3 | The maintenance window will be automatically removed when the maintenance ends. 4 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceWindow/help-startTime.properties: -------------------------------------------------------------------------------- 1 | startTime=The date and time when the maintenance starts in the time zone of the Jenkins controller JVM (currently {0}).
\ 2 | Formatted as yyyy-MM-dd HH:mm
\ 3 | From this point in time Jenkins will not allow new builds to execute on this agent. 4 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/RecurringMaintenanceWindow/help-startTimeSpec.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | ${%startTimeSpec(currentTimeZone.getID())} 6 |
7 |
-------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/AgentMaintenanceRetentionStrategy/help.html: -------------------------------------------------------------------------------- 1 |
2 | Take this agent offline during dedicated time windows, e.g. due to maintenance activities. Outside of an active maintenance window, the "Regular Availability" is applied.
3 | Exact behaviour what to do with running builds can be individually defined for each maintenanance window. 4 |
-------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/flatpickr.js: -------------------------------------------------------------------------------- 1 | Behaviour.specify(".am__flatpickr", "am-flatpickr", 0, function(fp) { 2 | flatpickr(fp, { 3 | allowInput: true, 4 | enableTime: true, 5 | wrap: true, 6 | clickOpens: false, 7 | dateFormat: "Y-m-d H:i", 8 | time_24hr: true, 9 | minDate: fp.dataset.now, 10 | static: true, 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/Messages.properties: -------------------------------------------------------------------------------- 1 | MaintenanceAction.maintenanceWindows=Maintenance Windows 2 | MaintenanceAction.view=View Maintenance Windows 3 | AgentMaintenanceRetentionStrategy.displayName=Take agent offline during maintenance, otherwise use other availability 4 | MaintenanceLink.displayName=Agent Maintenances 5 | MaintenanceLink.description=List maintenance windows of all agents. -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.13 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/webapp/help/help-duration.html: -------------------------------------------------------------------------------- 1 |
2 | The downtime in minutes for the maintenance window. 3 | The format is '<integer>' in minutes, '<integer><unit>' or a combination of '<integer><unit>', 4 | where <unit> is one of 'm' (minutes), 'h' (hours) or 'd' (days). 5 | Example: '1d 4h 30m' means one day, four hours and 30 minutes which evaluates to 1710 minutes. 6 | 1710 is then the value that will be displayed later. 7 |
-------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | jobs: 11 | maven-cd: 12 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 13 | secrets: 14 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 15 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 16 | -------------------------------------------------------------------------------- /src/main/webapp/help/help-maxWaitMinutes.html: -------------------------------------------------------------------------------- 1 | The time in minutes to wait before aborting running builds.
2 | Set to a negative value to wait indefinitely.
3 | The format is '<integer>' in minutes, '<integer><unit>' or a combination of '<integer><unit>', 4 | where <unit> is one of 'm' (minutes), 'h' (hours) or 'd' (days). 5 | Example: '1d 4h 30m' means one day, four hours and 30 minutes which evaluates to 1710 minutes. 6 | 1710 is then the value that will be displayed later. -------------------------------------------------------------------------------- /.build-config/checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 10 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceInterruption.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import jenkins.model.CauseOfInterruption; 4 | 5 | /** Agent is down for maintenance. */ 6 | public class MaintenanceInterruption extends CauseOfInterruption { 7 | private static final long serialVersionUID = 1L; 8 | 9 | @Override 10 | public String getShortDescription() { 11 | return "Agent is going down for scheduled maintenance"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceConfiguration/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/webapp/help/help-maxWaitMinutes_de.html: -------------------------------------------------------------------------------- 1 | Die Wartezeit in Minuten, bevor laufende Builds abgebrochen werden.
2 | Legen Sie einen negativen Wert fest, um unbegrenzt zu warten.
3 | Das Format ist '<Ganzzahl>' in Minuten, '<Ganzzahl><Einheit>' oder eine Kombination aus '<Ganzzahl><Einheit>', 4 | wobei <Einheit> entweder 'm' (Minuten), 'h' (Stunden) oder 'd' (Tage) sein muss. Beispiel: '1d 4h 30m' bedeutet einen Tag, 5 | vier Stunden und 30 Minuten, was 1710 Minuten ergibt. 1710 ist dann der Wert der später angezeigt wird. -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceConfiguration/help-injectRetentionstrategy.html: -------------------------------------------------------------------------------- 1 |
2 | Inject the agent maintenance availability automatically to all newly created and updated agents if not already configured.
3 | For existing agents, the currently defined availability will be set as the default strategy.
4 | This will not inject the agent maintenance availability to cloud agents.
5 | Use the button Inject to set the agent maintenance availability for all existing agents if not already configured.
6 | Use the button Remove to recover the originally defined availability. 7 |
-------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | pull_request: 7 | types: [ opened, synchronize, reopened ] 8 | workflow_dispatch: 9 | 10 | permissions: 11 | security-events: write 12 | contents: read 13 | actions: read 14 | 15 | jobs: 16 | security-scan: 17 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 18 | with: 19 | java-cache: '' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 20 | # java-version: 11 # What version of Java to set up for the build. 21 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceConfiguration/help-injectRetentionstrategy_de.html: -------------------------------------------------------------------------------- 1 |
2 | Injiziere die Agentenwartungsverfügbarkeit automatisch in alle neu erstellten und aktualisierten Agenten, 3 | falls diese noch nicht konfiguriert ist.
4 | Für bestehende Agenten wird die aktuell definierte Verfügbarkeit als Standardstrategie eingestellt.
5 | Dies fügt die Agentenwartungsverfügbarkeit nicht zu Cloud-Agenten hinzu.
6 | Verwenden Sie die Schaltfläche Einfügen, um die Agentenwartungsverfügbarkeit für alle vorhandenen Agenten festzulegen, 7 | falls noch nicht konfiguriert.
8 | Verwenden Sie die Schaltfläche Entfernen, um die ursprünglich definierte Verfügbarkeit wiederherzustellen.
9 |
-------------------------------------------------------------------------------- /src/test/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MigrationTest/nodes/agent/maintenance-windows.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | test 5 | true 6 | true 7 | -1 8 | ich 9 | 69911276-9e33-4e10-b91b-10533feb0008 10 | 2020-03-08T13:17:00 11 | 2099-03-09T13:17:00 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceOfflineCause/cause.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 |
7 |
8 | 9 | ${%Start Time} : ${it.startTime}
10 | ${%End Time} : ${it.endTime}
11 | ${%Reason} : ${it.reason} 12 |
13 | 14 | ${%maintenanceover} 15 | 16 |
17 |
18 |
-------------------------------------------------------------------------------- /src/main/resources/lib/permissions/hasAnyPermission.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Renders the body only if the current user has any of the specified permissions 5 | 6 | By default it will reuse the current context. 7 | If the provided value does not inherit from hudson.security.AccessControlled, 8 | the tag will look for the first ancestor satisfying the condition. 9 | The hasAnyPermission will be performed against that value. 10 | 11 | 12 | permissions object to check. If this is null, the body will be also rendered. 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/lib/permissions/hasNoPermission.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Renders the body only if the current user has any of the specified permissions 5 | 6 | By default it will reuse the current context. 7 | If the provided value does not inherit from hudson.security.AccessControlled, 8 | the tag will look for the first ancestor satisfying the condition. 9 | The hasAnyPermission will be performed against that value. 10 | 11 | 12 | permissions object to check. If this is null, the body will be also rendered. 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/RecurringTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | 6 | import hudson.model.Slave; 7 | import org.junit.jupiter.api.Test; 8 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 9 | 10 | /** Test recurring maintenance windows. */ 11 | @WithJenkins 12 | class RecurringTest extends BaseIntegrationTest { 13 | 14 | @Test 15 | void recurringMaintenanceInjectsMaintenance() throws Exception { 16 | String agentName = "recurring"; 17 | Slave agent = getAgent(agentName); 18 | RecurringMaintenanceWindow rw = new RecurringMaintenanceWindow("0 2 * * *", 19 | "test", true, true, "10m", "60m", "test", null, 0); 20 | maintenanceHelper.addRecurringMaintenanceWindow(agent.getNodeName(), rw); 21 | triggerCheckCycle(agent); 22 | assertThat(maintenanceHelper.hasMaintenanceWindows(agentName), is(true)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "37 10 * * 2" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript, java ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v5 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v4 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v4 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v4 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /src/test/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MigrationTest/nodes/agent/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | agent 4 | 5 | \mock-agents\agent 6 | 1 7 | NORMAL 8 | 9 | 10 | 5 11 | 1 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/MigrationTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.instanceOf; 5 | import static org.hamcrest.Matchers.is; 6 | 7 | import hudson.model.Node; 8 | import hudson.slaves.RetentionStrategy; 9 | import java.io.IOException; 10 | import org.junit.jupiter.api.Test; 11 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 12 | import org.jvnet.hudson.test.recipes.LocalData; 13 | 14 | /** 15 | * Tests that the old data format for maintenance windows is properly read. 16 | */ 17 | @WithJenkins 18 | class MigrationTest extends BaseIntegrationTest { 19 | 20 | @Test 21 | @LocalData 22 | void readOldData() throws IOException { 23 | Node agent = rule.jenkins.getNode("agent"); 24 | RetentionStrategy retentionStrategy = agent.toComputer().getRetentionStrategy(); 25 | assertThat(retentionStrategy, instanceOf(AgentMaintenanceRetentionStrategy.class)); 26 | assertThat(MaintenanceHelper.getInstance().hasMaintenanceWindows("agent"), is(true)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceActionFactory.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.model.Action; 6 | import hudson.slaves.AbstractCloudComputer; 7 | import hudson.slaves.SlaveComputer; 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.List; 11 | import jenkins.model.TransientActionFactory; 12 | 13 | /** Inject the action link to agents. */ 14 | @Extension 15 | public class MaintenanceActionFactory extends TransientActionFactory { 16 | 17 | @Override 18 | @NonNull 19 | public Collection createFor(@NonNull SlaveComputer target) { 20 | List result = new ArrayList<>(); 21 | if (!(target instanceof AbstractCloudComputer) 22 | && target.getActions().stream().noneMatch(x -> x instanceof MaintenanceAction)) { 23 | MaintenanceAction action = new MaintenanceAction(target); 24 | result.add(action); 25 | target.addAction(new MaintenanceAction(target)); 26 | } 27 | return result; 28 | } 29 | 30 | @Override 31 | public Class type() { 32 | return SlaveComputer.class; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/agent-maintenance.css: -------------------------------------------------------------------------------- 1 | /* The popup form - hidden by default */ 2 | .am__modal { 3 | display: none; 4 | } 5 | 6 | .am__table 7 | { 8 | width: auto; 9 | margin-bottom: 0px; 10 | } 11 | 12 | .am__action-delete, 13 | .am__action-delete-recurring, 14 | .am__link-delete 15 | { 16 | cursor: pointer; 17 | } 18 | 19 | .am__table > tbody > tr.active > td 20 | { 21 | background: var(--medium-grey); 22 | } 23 | 24 | 25 | .am__table > thead > tr > th 26 | { 27 | padding-right: 5px; 28 | padding-left: 10px; 29 | } 30 | 31 | .am__table > tbody > tr > td 32 | { 33 | padding-right: 5px; 34 | padding-left: 10px; 35 | } 36 | 37 | .am__checkbox 38 | { 39 | vertical-align: middle; 40 | } 41 | 42 | .right { 43 | text-align: right; 44 | } 45 | 46 | .am__select { 47 | color: var(--link-color); 48 | text-decoration: underline; 49 | cursor: pointer; 50 | } 51 | 52 | .am__table-icon { 53 | margin-bottom: 2px; 54 | } 55 | 56 | .jenkins-checkbox { 57 | vertical-align: middle; 58 | } 59 | 60 | .am__div--break { 61 | flex-basis: 100%; 62 | height: 0; 63 | } 64 | 65 | #am__div--select { 66 | flex-grow: 1; 67 | } 68 | 69 | .jenkins-buttons-row { 70 | flex-wrap: wrap; 71 | row-gap: 5px; 72 | } 73 | 74 | .jenkins-dialog__contents { 75 | padding-top: 5px; 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceDefinitions.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import java.util.Set; 4 | import java.util.SortedSet; 5 | 6 | /** 7 | * Container that holds the scheduled and recurring maintenance windows for an agent. 8 | */ 9 | public class MaintenanceDefinitions { 10 | private final SortedSet scheduled; 11 | private final Set recurring; 12 | 13 | /** 14 | * Create definitions container. 15 | * 16 | * @param scheduled A set of scheduled maintenance windows 17 | * @param recurring A set of recurring maintenance windows 18 | */ 19 | public MaintenanceDefinitions(SortedSet scheduled, Set recurring) { 20 | this.scheduled = scheduled; 21 | this.recurring = recurring; 22 | } 23 | 24 | /** 25 | * Get the scheduled maintenance windows. 26 | * 27 | * @return Set of scheduled maintenance windows 28 | */ 29 | public SortedSet getScheduled() { 30 | return scheduled; 31 | } 32 | 33 | /** 34 | * Get the recurring maintenance windows. 35 | * 36 | * @return Set of recurring maintenance windows 37 | */ 38 | public Set getRecurring() { 39 | return recurring; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/AgentMaintenanceRetentionStrategy/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceOfflineCause.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import hudson.slaves.OfflineCause; 4 | 5 | /** Offline cause because of a maintenance. */ 6 | public class MaintenanceOfflineCause extends OfflineCause { 7 | 8 | private MaintenanceWindow maintenanceWindow; 9 | private final String computerName; 10 | 11 | public MaintenanceOfflineCause(MaintenanceWindow maintenanceWindow, String computerName) { 12 | this.maintenanceWindow = maintenanceWindow; 13 | this.computerName = computerName; 14 | } 15 | 16 | private void updateMaintenanceWindow() { 17 | MaintenanceWindow newMaintenanceWindow = MaintenanceHelper.getInstance().getMaintenanceWindow(computerName, maintenanceWindow.getId()); 18 | if (newMaintenanceWindow != null) { 19 | maintenanceWindow = newMaintenanceWindow; 20 | } 21 | } 22 | 23 | public String getStartTime() { 24 | updateMaintenanceWindow(); 25 | return maintenanceWindow.getStartTime(); 26 | } 27 | 28 | public String getEndTime() { 29 | updateMaintenanceWindow(); 30 | return maintenanceWindow.getEndTime(); 31 | } 32 | 33 | public String getReason() { 34 | updateMaintenanceWindow(); 35 | return maintenanceWindow.getReason(); 36 | } 37 | 38 | public boolean isTakeOnline() { 39 | return maintenanceWindow.isTakeOnline(); 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return getReason(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Agent Maintenance Plugin 2 | 3 | Plugin source code is hosted on [GitHub](https://github.com/jenkinsci/agent-maintenance-plugin). 4 | New feature proposals and bug fix proposals should be submitted as 5 | [GitHub pull requests](https://help.github.com/articles/creating-a-pull-request). 6 | Your pull request will be evaluated by the [Jenkins job](https://ci.jenkins.io/job/Plugins/job/agent-maintenance-plugin/). 7 | 8 | Before submitting your change, please assure that you've added tests that verify the change. 9 | 10 | ## Code Coverage 11 | 12 | Code coverage reporting is available as a maven target. 13 | Please try to improve code coverage with tests when you submit. 14 | 15 | * `mvn -P enable-jacoco clean install jacoco:report` to report code coverage 16 | 17 | ## Static Analysis 18 | 19 | Please don't introduce new spotbugs output. 20 | 21 | * `mvn spotbugs:check` to analyze project using [Spotbugs](https://spotbugs.github.io) 22 | * `mvn spotbugs:gui` to review report using GUI 23 | 24 | ## Code Formatting 25 | 26 | Code formatting is checked by checkstyle. If the formatting is not correct, the build will fail. 27 | The rules are basically the Google Java formatting rules with some smaller relaxations. 28 | 29 | 30 | ## File format 31 | 32 | Files in the repository are in Unix format (LF line terminators). 33 | Please continue using Unix file format for consistency. 34 | 35 | ## Reporting Issues 36 | 37 | Report issues in the [Jenkins issue tracker](https://www.jenkins.io/participate/report-issue/redirect/#(tbd)). -------------------------------------------------------------------------------- /src/main/resources/images/symbols/maintenance.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Computer Maintenance 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceNodeListener.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.model.Node; 6 | import hudson.model.Slave; 7 | import hudson.slaves.AbstractCloudSlave; 8 | import jenkins.model.NodeListener; 9 | 10 | /** Listener to react on events for agents. */ 11 | @Extension 12 | public class MaintenanceNodeListener extends NodeListener { 13 | MaintenanceHelper helper = MaintenanceHelper.getInstance(); 14 | 15 | @Override 16 | protected void onCreated(@NonNull Node node) { 17 | if (node instanceof Slave && !(node instanceof AbstractCloudSlave)) { 18 | helper.createAgent(node.getNodeName()); 19 | if (MaintenanceConfiguration.getInstance().isInjectRetentionStrategy()) { 20 | helper.injectRetentionStrategy(node.toComputer()); 21 | } 22 | } 23 | } 24 | 25 | @Override 26 | protected void onDeleted(@NonNull Node node) { 27 | if (node instanceof Slave) { 28 | helper.deleteAgent(node.getNodeName()); 29 | } 30 | } 31 | 32 | @Override 33 | protected void onUpdated(@NonNull Node oldNode, @NonNull Node newNode) { 34 | if (newNode instanceof Slave && !(newNode instanceof AbstractCloudSlave)) { 35 | if (!oldNode.getNodeName().equals(newNode.getNodeName())) { 36 | helper.renameAgent(oldNode.getNodeName(), newNode.getNodeName()); 37 | } 38 | if (MaintenanceConfiguration.getInstance().isInjectRetentionStrategy()) { 39 | helper.injectRetentionStrategy(newNode.toComputer()); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: agent-maintenance-plugin 3 | Upstream-Contact: m.winter_at_sap.com 4 | Source: https://github.com/sap/agent-maintenance-plugin 5 | Disclaimer: The code in this project may include calls to APIs ("API Calls") of 6 | SAP or third-party products or services developed outside of this project 7 | ("External Products"). 8 | "APIs" means application programming interfaces, as well as their respective 9 | specifications and implementing code that allows software to communicate with 10 | other software. 11 | API Calls to External Products are not licensed under the open source license 12 | that governs this project. The use of such API Calls and related External 13 | Products are subject to applicable additional agreements with the relevant 14 | provider of the External Products. In no event shall the open source license 15 | that governs this project grant any rights in or to any External Products,or 16 | alter, expand or supersede any terms of the applicable additional agreements. 17 | If you have a valid license agreement with SAP for the use of a particular SAP 18 | External Product, then you may make use of any API Calls included in this 19 | project's code for that SAP External Product, subject to the terms of such 20 | license agreement. If you do not have a valid license agreement for the use of 21 | a particular SAP External Product, then you may only make use of any API Calls 22 | in this project for that SAP External Product for your internal, non-productive 23 | and non-commercial test and evaluation of such API Calls. Nothing herein grants 24 | you any rights to use or access any SAP External Product, or provide any third 25 | parties the right to use of access any SAP External Product, through API Calls. 26 | 27 | Files: ** 28 | Copyright: 2022 SAP SE or an SAP affiliate company and agent-maintenance-plugin contributors 29 | License: Apache-2.0 30 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/RecurringMaintenanceWindow/help-startTimeSpec.properties: -------------------------------------------------------------------------------- 1 | startTimeSpec=This field follows the syntax of cron (with some differences) and deviates from the syntax used at other places in Jenkins:
\ 2 |
MINUTE HOUR DOM MONTH DOW
\ 3 |
    \ 4 |
  • A field may be an asterisk (*), which always stands for "first-last". For the "day of the month" or "day of the week" fields, a question mark (?) may be used instead of an asterisk.
  • \ 5 |
  • Ranges of numbers are expressed by two numbers separated with a hyphen (-). The specified range is inclusive.
  • \ 6 |
  • Following a range (or *) with /n specifies the interval of the number''s value through the range.
  • \ 7 |
  • English names can also be used for the "month" and "day of week" fields. Use the first three letters of the particular day or month (case does not matter).
  • \ 8 |
  • The "day of month" and "day of week" fields can contain a L-character, which stands for "last", and has a different meaning in each field:\ 9 |
      \ 10 |
    • In the "day of month" field, L stands for "the last day of the month". If followed by an negative offset (i.e. L-n), it means "nth-to-last day of the month". If followed by W (i.e. LW), it means "the last weekday of the month".
    • \ 11 |
    • In the "day of week" field, dL or DDDL stands for "the last day of week d (or DDD) in the month".
    • \ 12 |
    \ 13 |
  • The "day of month" field can be nW, which stands for "the nearest weekday to day of the month n". If n falls on Saturday, this yields the Friday before it. If n falls on Sunday, this yields the Monday after, which also happens if n is 1 and falls on a Saturday (i.e. 1W stands for "the first weekday of the month").
  • \ 14 |
  • The "day of week" field can be d#n (or DDD#n), which stands for "the n-th day of week d (or DDD) in the month".
  • \ 15 |
\ 16 | The times will be in the time zone of the Jenkins controller JVM (currently {0}).
17 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/RecurringMaintenanceWindow/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceHelperTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | 6 | import hudson.model.Slave; 7 | import java.util.Set; 8 | import org.junit.jupiter.api.Test; 9 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 10 | 11 | /** 12 | * Test the helper. 13 | */ 14 | @WithJenkins 15 | class MaintenanceHelperTest extends BaseIntegrationTest { 16 | 17 | @Test 18 | void getMaintenanceWindowsExistingAgent() throws Exception { 19 | Slave agent = rule.createOnlineSlave(); 20 | String agentName = agent.getNodeName(); 21 | Set mwSet = maintenanceHelper.getMaintenanceWindows(agentName); 22 | assertThat(mwSet.size(), is(0)); 23 | MaintenanceWindow mw = new MaintenanceWindow("1970-01-01 11:00", "2099-12-31 23:59", "test", true, true, "10", "user", null); 24 | mwSet.add(mw); 25 | mwSet = maintenanceHelper.getMaintenanceWindows(agentName); 26 | assertThat(mwSet.size(), is(1)); 27 | } 28 | 29 | @Test 30 | void getMaintenanceWindowsNonExistingAgent() throws Exception { 31 | String agentName = "notExisting"; 32 | Set mwSet = maintenanceHelper.getMaintenanceWindows(agentName); 33 | assertThat(mwSet.size(), is(0)); 34 | MaintenanceWindow mw = new MaintenanceWindow("1970-01-01 11:00", "2099-12-31 23:59", "test", true, true, "10", "user", null); 35 | mwSet.add(mw); 36 | mwSet = maintenanceHelper.getMaintenanceWindows(agentName); 37 | assertThat(mwSet.size(), is(0)); 38 | } 39 | 40 | @Test 41 | void parseDurationString() { 42 | assertThat(MaintenanceHelper.parseDurationString("10"), is(10)); 43 | assertThat(MaintenanceHelper.parseDurationString("10m"), is(10)); 44 | assertThat(MaintenanceHelper.parseDurationString("1h"), is(60)); 45 | assertThat(MaintenanceHelper.parseDurationString("2h 10m"), is(130)); 46 | assertThat(MaintenanceHelper.parseDurationString("1d 1h 30m"), is(1530)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceAction/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 |

${%Edit Planned Maintenances}

8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /src/test/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MigrationTest/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.346.1 5 | 2 6 | NORMAL 7 | true 8 | 9 | USER:hudson.model.Hudson.Administer:admin 10 | 11 | 12 | admin 13 | false 14 | 15 | 16 | 17 | false 18 | 19 | ${JENKINS_HOME}/workspace/${ITEM_FULL_NAME} 20 | ${ITEM_ROOTDIR}/builds 21 | 22 | 23 | 24 | 25 | 26 | 0 27 | 28 | 29 | 30 | all 31 | false 32 | false 33 | 34 | 35 | 36 | all 37 | 0 38 | 39 | 40 | false 41 | 42 | 43 | 44 | false 45 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/BaseIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import hudson.model.Slave; 4 | import hudson.slaves.RetentionStrategy; 5 | import hudson.slaves.RetentionStrategy.Always; 6 | import hudson.slaves.SlaveComputer; 7 | import java.time.LocalDateTime; 8 | import java.util.concurrent.TimeUnit; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.jvnet.hudson.test.JenkinsRule; 11 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 12 | 13 | /** 14 | * Base class for tests of the retention strategy. 15 | */ 16 | @WithJenkins 17 | abstract class BaseIntegrationTest { 18 | 19 | protected JenkinsRule rule; 20 | 21 | protected MaintenanceHelper maintenanceHelper = MaintenanceHelper.getInstance(); 22 | 23 | @BeforeEach 24 | void setup(JenkinsRule rule) throws Exception { 25 | this.rule = rule; 26 | } 27 | 28 | protected Slave getAgent(String name) throws Exception { 29 | return getAgent(name, null); 30 | } 31 | 32 | protected Slave getAgent(String name, RetentionStrategy innerStrategy) throws Exception { 33 | Slave agent = rule.createSlave(name, null, null); 34 | rule.waitOnline(agent); 35 | if (innerStrategy == null) { 36 | innerStrategy = new Always(); 37 | } 38 | AgentMaintenanceRetentionStrategy ams = new AgentMaintenanceRetentionStrategy(innerStrategy); 39 | agent.setRetentionStrategy(ams); 40 | 41 | return agent; 42 | } 43 | 44 | protected void waitForDisconnect(Slave agent, MaintenanceWindow mw) throws Exception { 45 | LocalDateTime timeout = LocalDateTime.now().plusMinutes(4); 46 | while (agent.getChannel() != null) { 47 | TimeUnit.SECONDS.sleep(10); 48 | triggerCheckCycle(agent); 49 | LocalDateTime now = LocalDateTime.now(); 50 | if (now.isAfter(timeout)) { 51 | String active = "unknown"; 52 | if (mw != null) { 53 | active = "" + mw.isMaintenanceScheduled(); 54 | } 55 | throw new Exception("Agent did not disconnect within 4 minutes. Active: " + active); 56 | } 57 | } 58 | } 59 | 60 | protected void triggerCheckCycle(Slave agent) { 61 | SlaveComputer computer = (SlaveComputer) agent.toComputer(); 62 | if (computer != null) { 63 | computer.getRetentionStrategy().check(computer); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceWindowTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | 6 | import com.sap.prd.jenkins.plugins.agent_maintenance.MaintenanceWindow.DescriptorImpl; 7 | import hudson.util.FormValidation; 8 | import org.junit.jupiter.api.Test; 9 | 10 | /** Tests for the maintenance window. */ 11 | class MaintenanceWindowTest { 12 | 13 | @Test 14 | void testDoCheckStartTime() { 15 | DescriptorImpl d = new DescriptorImpl(); 16 | assertThat(d.doCheckStartTime("2022-01-01 12:00", "").kind, is(FormValidation.Kind.ERROR)); 17 | assertThat(d.doCheckStartTime("", "2022-01-01 12:00").kind, is(FormValidation.Kind.ERROR)); 18 | assertThat(d.doCheckStartTime("2022-01-01 12:00", "2022-01-01 10:00").kind, is(FormValidation.Kind.WARNING)); 19 | assertThat(d.doCheckStartTime("2022-01-01 12:00", "2022-01-02 12:00").kind, is(FormValidation.Kind.OK)); 20 | } 21 | 22 | @Test 23 | void testEquals() { 24 | MaintenanceWindow m1 = new MaintenanceWindow("2022-01-01 12:00", "2022-01-02 12:00", "test", false, false, "0", "user", "id"); 25 | MaintenanceWindow m2 = new MaintenanceWindow("2022-01-01 12:00", "2022-01-02 12:00", "test", false, false, "0", "user", "id"); 26 | MaintenanceWindow m3 = new MaintenanceWindow("2022-01-01 12:00", "2022-01-02 12:00", "test", false, false, "0", "user2", "id"); 27 | MaintenanceWindow m4 = new MaintenanceWindow("2022-01-01 12:00", "2022-01-02 12:00", "test", false, false, "0", "user", "id2"); 28 | MaintenanceWindow m5 = new MaintenanceWindow("2022-01-01 12:00", "2022-01-02 11:00", "test", false, false, "350", "user", "id"); 29 | MaintenanceWindow m6 = new MaintenanceWindow("2022-01-01 11:00", "2022-01-02 12:00", "test", false, false, "0", "user", "id"); 30 | MaintenanceWindow m7 = new MaintenanceWindow("2022-01-01 12:00", "2022-01-02 12:00", "test", true, false, "0", "user", "id"); 31 | MaintenanceWindow m8 = new MaintenanceWindow("2022-01-01 12:00", "2022-01-02 12:00", "test", false, true, "0", "user", "id"); 32 | MaintenanceWindow m9 = new MaintenanceWindow("2022-01-01 12:00", "2022-01-02 12:00", "test2", false, false, "0", "user", "id"); 33 | 34 | assertThat(m1.equals(m2), is(true)); 35 | assertThat(m1.equals(m3), is(true)); 36 | assertThat(m1.equals(m4), is(true)); 37 | assertThat(m1.equals(m5), is(false)); 38 | assertThat(m1.equals(m6), is(false)); 39 | assertThat(m1.equals(m7), is(false)); 40 | assertThat(m1.equals(m8), is(false)); 41 | assertThat(m1.equals(m9), is(false)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceOfflineCauseTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import static com.sap.prd.jenkins.plugins.agent_maintenance.MaintenanceWindow.DATE_FORMATTER; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.is; 6 | 7 | import hudson.model.Slave; 8 | import hudson.slaves.RetentionStrategy; 9 | import java.time.LocalDateTime; 10 | import java.util.SortedSet; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.jvnet.hudson.test.JenkinsRule; 14 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 15 | 16 | /** Tests the offline cause. */ 17 | @WithJenkins 18 | class MaintenanceOfflineCauseTest extends BaseIntegrationTest { 19 | 20 | private Slave agent; 21 | 22 | /** 23 | * Setup agent. 24 | * 25 | * @throws Exception in case of an error 26 | */ 27 | @Override 28 | @BeforeEach 29 | void setup(JenkinsRule rule) throws Exception { 30 | super.setup(rule); 31 | agent = rule.createOnlineSlave(); 32 | AgentMaintenanceRetentionStrategy strategy = 33 | new AgentMaintenanceRetentionStrategy(new RetentionStrategy.Always()); 34 | agent.setRetentionStrategy(strategy); 35 | } 36 | 37 | @Test 38 | void offlineCauseIsUpdated() throws Exception { 39 | LocalDateTime start = LocalDateTime.now().minusMinutes(1); 40 | LocalDateTime end = start.plusMinutes(5); 41 | MaintenanceWindow mw = 42 | new MaintenanceWindow( 43 | start.format(DATE_FORMATTER), 44 | end.format(DATE_FORMATTER), 45 | "test", 46 | true, 47 | true, 48 | "5", 49 | "test", 50 | null); 51 | maintenanceHelper.addMaintenanceWindow(agent.getNodeName(), mw); 52 | MaintenanceOfflineCause moc = (MaintenanceOfflineCause) mw.getOfflineCause(agent.getNodeName()); 53 | assertThat(moc.getReason(), is("test")); 54 | MaintenanceWindow updated = 55 | new MaintenanceWindow( 56 | start.format(DATE_FORMATTER), 57 | end.format(DATE_FORMATTER), 58 | "changed", 59 | true, 60 | true, 61 | "5", 62 | "test", 63 | mw.getId()); 64 | 65 | MaintenanceDefinitions mwdefinitions = MaintenanceHelper.getInstance().getMaintenanceDefinitions(agent.getNodeName()); 66 | synchronized (mwdefinitions) { 67 | SortedSet mwList = mwdefinitions.getScheduled(); 68 | mwList.clear(); 69 | mwList.add(updated); 70 | MaintenanceHelper.getInstance().saveMaintenanceWindows(agent.getNodeName(), mwdefinitions); 71 | } 72 | assertThat(moc.getReason(), is("changed")); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceWindow/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 13 | 14 | 17 |
18 |
19 | 20 |
21 | 23 | 26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
-------------------------------------------------------------------------------- /src/main/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import hudson.Extension; 4 | import hudson.model.Node; 5 | import hudson.model.Slave; 6 | import hudson.slaves.AbstractCloudSlave; 7 | import hudson.util.HttpResponses; 8 | import jenkins.model.GlobalConfiguration; 9 | import jenkins.model.Jenkins; 10 | import org.jenkinsci.Symbol; 11 | import org.kohsuke.stapler.DataBoundConstructor; 12 | import org.kohsuke.stapler.HttpResponse; 13 | import org.kohsuke.stapler.StaplerResponse2; 14 | import org.kohsuke.stapler.verb.POST; 15 | 16 | /** The global configuration of the plugin. */ 17 | @Extension 18 | @Symbol("agent-maintenance") 19 | public class MaintenanceConfiguration extends GlobalConfiguration { 20 | private boolean injectRetentionStrategy; 21 | 22 | @DataBoundConstructor 23 | public MaintenanceConfiguration() { 24 | load(); 25 | } 26 | 27 | public void setInjectRetentionStrategy(boolean injectRetentionStrategy) { 28 | this.injectRetentionStrategy = injectRetentionStrategy; 29 | save(); 30 | } 31 | 32 | public boolean isInjectRetentionStrategy() { 33 | return injectRetentionStrategy; 34 | } 35 | 36 | public static MaintenanceConfiguration getInstance() { 37 | return GlobalConfiguration.all().get(MaintenanceConfiguration.class); 38 | } 39 | 40 | /** 41 | * Called when UI button to inject strategy to all agents is pressed. 42 | * 43 | * @param rsp Stapler Response 44 | * @return A HttpResponse 45 | */ 46 | @POST 47 | public HttpResponse doInject(StaplerResponse2 rsp) { 48 | Jenkins.get().checkPermission(Jenkins.ADMINISTER); 49 | int counter = 0; 50 | for (Node node : Jenkins.get().getNodes()) { 51 | if (node instanceof Slave && !(node instanceof AbstractCloudSlave)) { 52 | if (MaintenanceHelper.getInstance().injectRetentionStrategy(node.toComputer())) { 53 | counter++; 54 | } 55 | } 56 | } 57 | String message = "
Injected maintenance strategy to " + counter + " agents
"; 58 | return HttpResponses.literalHtml(message); 59 | } 60 | 61 | /** 62 | * Called when UI button to remove strategy from all agents is pressed. 63 | * 64 | * @param rsp Stapler Response 65 | * @return A HttpResponse 66 | */ 67 | @POST 68 | public HttpResponse doRemove(StaplerResponse2 rsp) { 69 | Jenkins.get().checkPermission(Jenkins.ADMINISTER); 70 | int counter = 0; 71 | for (Node node : Jenkins.get().getNodes()) { 72 | if (node instanceof Slave && !(node instanceof AbstractCloudSlave)) { 73 | if (MaintenanceHelper.getInstance().removeRetentionStrategy(node.toComputer())) { 74 | counter++; 75 | } 76 | } 77 | } 78 | String message = "
Removed maintenance strategy from " + counter + " agents
"; 79 | return HttpResponses.literalHtml(message); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/BasePermissionChecks.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import hudson.model.Computer; 4 | import hudson.model.Slave; 5 | import hudson.slaves.RetentionStrategy.Always; 6 | import hudson.slaves.RetentionStrategy.Demand; 7 | import java.io.IOException; 8 | import org.jenkinsci.plugins.matrixauth.AuthorizationMatrixNodeProperty; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 11 | 12 | /** Base class for permission checks. */ 13 | @WithJenkins 14 | abstract class BasePermissionChecks extends PermissionSetup { 15 | 16 | protected static Slave agent; 17 | protected static Slave agentRestricted; 18 | protected static String maintenanceId; 19 | protected static String maintenanceIdRestricted; 20 | protected static String maintenanceIdToDelete; 21 | protected static MaintenanceWindow maintenanceWindow; 22 | protected static MaintenanceWindow maintenanceWindowRestricted; 23 | protected static MaintenanceWindow maintenanceWindowToDelete; 24 | protected static String agentMaintenanceUrl; 25 | 26 | protected static MaintenanceWindow createMaintenanceWindow(Slave agent, String reason) throws IOException { 27 | 28 | MaintenanceWindow maintenanceWindow = 29 | new MaintenanceWindow( 30 | "1970-01-01 11:00", "2099-12-31 23:59", reason, true, true, "10", CONFIGURE, null); 31 | MaintenanceHelper.getInstance().addMaintenanceWindow(agent.getNodeName(), maintenanceWindow); 32 | return maintenanceWindow; 33 | } 34 | 35 | /** 36 | * Setup tests. 37 | * 38 | * @throws Exception when something goes wrong 39 | */ 40 | @BeforeEach 41 | public void setupAgents() throws Exception { 42 | agent = rule.createOnlineSlave(); 43 | agentRestricted = rule.createOnlineSlave(); 44 | agent.setRetentionStrategy(new AgentMaintenanceRetentionStrategy(new Demand(1, 2))); 45 | agentRestricted.setRetentionStrategy(new AgentMaintenanceRetentionStrategy(new Always())); 46 | 47 | maintenanceWindow = createMaintenanceWindow(agent, "test"); 48 | 49 | maintenanceId = maintenanceWindow.getId(); 50 | agentMaintenanceUrl = agent.toComputer().getUrl() + "maintenanceWindows"; 51 | 52 | maintenanceWindowRestricted = createMaintenanceWindow(agentRestricted, "test Restricted"); 53 | maintenanceIdRestricted = maintenanceWindowRestricted.getId(); 54 | 55 | maintenanceWindowToDelete = createMaintenanceWindow(agent, "test to delete"); 56 | maintenanceIdToDelete = maintenanceWindowToDelete.getId(); 57 | 58 | AuthorizationMatrixNodeProperty nodeProp = new AuthorizationMatrixNodeProperty(); 59 | AuthorizationMatrixNodeProperty nodePropRestricted = new AuthorizationMatrixNodeProperty(); 60 | 61 | // System read and computer configure on agent, but not on agentRestricted 62 | nodeProp.add(Computer.CONFIGURE, configure); 63 | nodeProp.add(Computer.DISCONNECT, disconnect); 64 | nodePropRestricted.add(Computer.EXTENDED_READ, configure); 65 | 66 | agent.getNodeProperties().add(nodeProp); 67 | agentRestricted.getNodeProperties().add(nodePropRestricted); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.instanceOf; 5 | import static org.hamcrest.Matchers.is; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | 8 | import hudson.model.Slave; 9 | import hudson.model.User; 10 | import hudson.security.ACL; 11 | import hudson.security.ACLContext; 12 | import hudson.security.AccessDeniedException3; 13 | import hudson.slaves.RetentionStrategy; 14 | import hudson.slaves.RetentionStrategy.Always; 15 | import org.junit.jupiter.api.BeforeEach; 16 | import org.junit.jupiter.api.Test; 17 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 18 | import org.kohsuke.stapler.StaplerResponse2; 19 | import org.mockito.Mock; 20 | 21 | /** 22 | * Test the Configuration. 23 | */ 24 | @WithJenkins 25 | class MaintenanceConfigurationTest extends PermissionSetup { 26 | 27 | @Mock 28 | private StaplerResponse2 rsp; 29 | 30 | private Slave agent; 31 | private Slave agent2; 32 | 33 | @BeforeEach 34 | void setup() throws Exception { 35 | agent = rule.createOnlineSlave(); 36 | agent2 = rule.createOnlineSlave(); 37 | } 38 | 39 | @Test 40 | void injectAddsToAgents() { 41 | 42 | MaintenanceConfiguration config = MaintenanceConfiguration.getInstance(); 43 | agent2.setRetentionStrategy(new AgentMaintenanceRetentionStrategy(new Always())); 44 | try (ACLContext ignored = ACL.as(User.getById(ADMIN, false))) { 45 | assertThat(agent.getRetentionStrategy(), is(RetentionStrategy.NOOP)); 46 | assertThat(agent2.getRetentionStrategy(), instanceOf(AgentMaintenanceRetentionStrategy.class)); 47 | config.doInject(rsp); 48 | assertThat(agent.getRetentionStrategy(), instanceOf(AgentMaintenanceRetentionStrategy.class)); 49 | assertThat(agent2.getRetentionStrategy(), instanceOf(AgentMaintenanceRetentionStrategy.class)); 50 | } 51 | } 52 | 53 | @Test 54 | void injectIsDeniedForReader() { 55 | MaintenanceConfiguration config = MaintenanceConfiguration.getInstance(); 56 | try (ACLContext ignored = ACL.as(User.getById(READER, false))) { 57 | assertThrows(AccessDeniedException3.class, () -> config.doInject(rsp)); 58 | } 59 | } 60 | 61 | @Test 62 | void removeFromAgents() { 63 | agent.setRetentionStrategy(new AgentMaintenanceRetentionStrategy(new Always())); 64 | MaintenanceConfiguration config = MaintenanceConfiguration.getInstance(); 65 | try (ACLContext ignored = ACL.as(User.getById(ADMIN, false))) { 66 | assertThat(agent.getRetentionStrategy(), instanceOf(AgentMaintenanceRetentionStrategy.class)); 67 | assertThat(agent2.getRetentionStrategy(), is(RetentionStrategy.NOOP)); 68 | config.doRemove(rsp); 69 | assertThat(agent.getRetentionStrategy(), instanceOf(Always.class)); 70 | assertThat(agent2.getRetentionStrategy(), is(RetentionStrategy.NOOP)); 71 | } 72 | } 73 | 74 | @Test 75 | void removeIsDeniedForReader() { 76 | MaintenanceConfiguration config = MaintenanceConfiguration.getInstance(); 77 | try (ACLContext ignored = ACL.as(User.getById(READER, false))) { 78 | assertThrows(AccessDeniedException3.class, () -> config.doRemove(rsp)); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/PermissionSetup.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import hudson.model.Computer; 4 | import hudson.security.HudsonPrivateSecurityRealm; 5 | import hudson.security.ProjectMatrixAuthorizationStrategy; 6 | import jenkins.model.Jenkins; 7 | import org.jenkinsci.plugins.matrixauth.AuthorizationType; 8 | import org.jenkinsci.plugins.matrixauth.PermissionEntry; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 11 | 12 | /** Base class for permission checks. */ 13 | @WithJenkins 14 | abstract class PermissionSetup extends BaseIntegrationTest { 15 | 16 | protected static final String READER = "reader"; 17 | protected static final String CONFIGURE = "configure"; 18 | protected static final String DISCONNECT = "disconnect"; 19 | protected static final String USER = "user"; 20 | protected static final String MANAGE = "manage"; 21 | protected static final String ADMIN = "admin"; 22 | 23 | protected static final PermissionEntry configure = new PermissionEntry(AuthorizationType.USER, CONFIGURE); 24 | protected static final PermissionEntry disconnect = new PermissionEntry(AuthorizationType.USER, DISCONNECT); 25 | 26 | /** 27 | * Setup tests. 28 | * 29 | * @throws Exception when something goes wrong 30 | */ 31 | @BeforeEach 32 | protected void setupPermissions() throws Exception { 33 | HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false, false, null); 34 | rule.jenkins.setSecurityRealm(realm); 35 | realm.createAccount(READER, READER); 36 | realm.createAccount(MANAGE, MANAGE); 37 | realm.createAccount(USER, USER); 38 | realm.createAccount(CONFIGURE, CONFIGURE); 39 | realm.createAccount(DISCONNECT, DISCONNECT); 40 | realm.createAccount(ADMIN, ADMIN); 41 | ProjectMatrixAuthorizationStrategy matrixAuth = new ProjectMatrixAuthorizationStrategy(); 42 | 43 | // System read and computer configure on agent, but not on agentRestricted 44 | matrixAuth.add(Jenkins.READ, configure); 45 | matrixAuth.add(Jenkins.SYSTEM_READ, configure); 46 | 47 | // System read and computer configure on agent, but not on agentRestricted 48 | matrixAuth.add(Jenkins.READ, disconnect); 49 | matrixAuth.add(Jenkins.SYSTEM_READ, disconnect); 50 | 51 | // system manage 52 | PermissionEntry manage = new PermissionEntry(AuthorizationType.USER, MANAGE); 53 | matrixAuth.add(Jenkins.READ, manage); 54 | matrixAuth.add(Jenkins.MANAGE, manage); 55 | 56 | // Administrator 57 | PermissionEntry admin = new PermissionEntry(AuthorizationType.USER, ADMIN); 58 | matrixAuth.add(Jenkins.ADMINISTER, admin); 59 | 60 | // system read 61 | PermissionEntry reader = new PermissionEntry(AuthorizationType.USER, READER); 62 | matrixAuth.add(Jenkins.READ, reader); 63 | matrixAuth.add(Jenkins.SYSTEM_READ, reader); 64 | matrixAuth.add(Computer.EXTENDED_READ, reader); 65 | 66 | // normal user 67 | PermissionEntry user = new PermissionEntry(AuthorizationType.USER, USER); 68 | matrixAuth.add(Jenkins.READ, user); 69 | 70 | rule.jenkins.setAuthorizationStrategy(matrixAuth); 71 | Jenkins.MANAGE.setEnabled(true); 72 | Jenkins.SYSTEM_READ.setEnabled(true); 73 | Computer.EXTENDED_READ.setEnabled(true); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceLinkTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.hamcrest.Matchers.notNullValue; 6 | import static org.hamcrest.Matchers.nullValue; 7 | 8 | import hudson.model.ManagementLink; 9 | import hudson.model.User; 10 | import hudson.security.ACL; 11 | import hudson.security.ACLContext; 12 | import java.util.List; 13 | import jenkins.model.Jenkins; 14 | import org.htmlunit.html.HtmlPage; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.extension.ExtendWith; 17 | import org.jvnet.hudson.test.JenkinsRule.WebClient; 18 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 19 | import org.kohsuke.stapler.StaplerRequest2; 20 | import org.kohsuke.stapler.StaplerResponse2; 21 | import org.mockito.Mock; 22 | import org.mockito.junit.jupiter.MockitoExtension; 23 | 24 | /** Access tests for the management link. */ 25 | @WithJenkins 26 | @ExtendWith(MockitoExtension.class) 27 | class MaintenanceLinkTest extends BasePermissionChecks { 28 | 29 | @Mock 30 | private StaplerRequest2 req; 31 | @Mock 32 | private StaplerResponse2 rsp; 33 | 34 | @Test 35 | void readPermissionHasNoAccess() throws Exception { 36 | WebClient w = rule.createWebClient(); 37 | w.login(USER); 38 | HtmlPage managePage = w.withThrowExceptionOnFailingStatusCode(false).goTo("agent-maintenances/"); 39 | assertThat(managePage.getWebResponse().getStatusCode(), is(403)); 40 | } 41 | 42 | @Test 43 | void systemReadPermissionDoesNotExposeDeleteLink() throws Exception { 44 | WebClient w = rule.createWebClient(); 45 | w.login(READER); 46 | HtmlPage managePage = w.goTo("agent-maintenances/"); 47 | assertThat(managePage.querySelector("#" + maintenanceId + " .am__link-delete"), is(nullValue())); 48 | assertThat(managePage.querySelector("#" + maintenanceIdRestricted + " .am__link-delete"), is(nullValue())); 49 | } 50 | 51 | @Test 52 | void managePermissionDoesNotExposeDeleteLink() throws Exception { 53 | WebClient w = rule.createWebClient(); 54 | w.login(MANAGE); 55 | HtmlPage managePage = w.goTo("agent-maintenances/"); 56 | assertThat(managePage.querySelector("#" + maintenanceId + " .am__link-delete"), is(nullValue())); 57 | assertThat(managePage.querySelector("#" + maintenanceIdRestricted + " .am__link-delete"), is(nullValue())); 58 | } 59 | 60 | @Test 61 | void deleteMaintenanceWindow() throws Exception { 62 | MaintenanceLink instance = null; 63 | List list = Jenkins.get().getManagementLinks(); 64 | for (ManagementLink link : list) { 65 | if (link instanceof MaintenanceLink) { 66 | instance = (MaintenanceLink) link; 67 | break; 68 | } 69 | } 70 | 71 | assertThat(instance, is(notNullValue())); 72 | 73 | WebClient w = rule.createWebClient(); 74 | w.login(ADMIN); 75 | HtmlPage managePage = w.goTo("agent-maintenances/"); 76 | assertThat(managePage.getElementById(maintenanceIdToDelete), is(notNullValue())); 77 | 78 | try (ACLContext ignored = ACL.as(User.getById(CONFIGURE, false))) { 79 | instance.deleteMaintenance(maintenanceIdToDelete, agent.getNodeName()); 80 | } 81 | 82 | managePage = w.goTo("agent-maintenances/"); 83 | assertThat(managePage.getElementById(maintenanceIdToDelete), is(nullValue())); 84 | assertThat(managePage.getElementById(maintenanceIdRestricted), is(notNullValue())); 85 | } 86 | 87 | @Test 88 | void configurePermissionDoesExposeDeleteLink() throws Exception { 89 | WebClient w = rule.createWebClient(); 90 | w.login(CONFIGURE); 91 | HtmlPage managePage = w.goTo("agent-maintenances/"); 92 | assertThat(managePage.querySelector("#" + maintenanceId + " .am__link-delete"), is(notNullValue())); 93 | assertThat(managePage.querySelector("#" + maintenanceIdRestricted + " .am__link-delete"), is(nullValue())); 94 | } 95 | 96 | @Test 97 | void disconnectPermissionDoesExposeDeleteLink() throws Exception { 98 | WebClient w = rule.createWebClient(); 99 | w.login(DISCONNECT); 100 | HtmlPage managePage = w.goTo("agent-maintenances/"); 101 | assertThat(managePage.querySelector("#" + maintenanceId + " .am__link-delete"), is(notNullValue())); 102 | assertThat(managePage.querySelector("#" + maintenanceIdRestricted + " .am__link-delete"), is(nullValue())); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceActionTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.instanceOf; 5 | import static org.hamcrest.Matchers.is; 6 | import static org.hamcrest.Matchers.notNullValue; 7 | import static org.hamcrest.Matchers.nullValue; 8 | import static org.junit.jupiter.api.Assertions.assertThrows; 9 | 10 | import hudson.model.User; 11 | import hudson.security.ACL; 12 | import hudson.security.ACLContext; 13 | import hudson.slaves.RetentionStrategy.Demand; 14 | import hudson.slaves.SlaveComputer; 15 | import org.htmlunit.html.HtmlPage; 16 | import org.junit.jupiter.api.Test; 17 | import org.junit.jupiter.api.extension.ExtendWith; 18 | import org.jvnet.hudson.test.JenkinsRule.WebClient; 19 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 20 | import org.kohsuke.stapler.StaplerRequest2; 21 | import org.kohsuke.stapler.StaplerResponse2; 22 | import org.mockito.Mock; 23 | import org.mockito.junit.jupiter.MockitoExtension; 24 | import org.springframework.security.access.AccessDeniedException; 25 | 26 | /** Tests for the action. */ 27 | @WithJenkins 28 | @ExtendWith(MockitoExtension.class) 29 | class MaintenanceActionTest extends BasePermissionChecks { 30 | 31 | @Mock 32 | private StaplerRequest2 req; 33 | @Mock 34 | private StaplerResponse2 rsp; 35 | 36 | @Test 37 | void readPermissionHasNoAccess() throws Exception { 38 | WebClient w = rule.createWebClient(); 39 | w.login(USER); 40 | HtmlPage managePage = w.withThrowExceptionOnFailingStatusCode(false).goTo(agentMaintenanceUrl); 41 | assertThat(managePage.getWebResponse().getStatusCode(), is(404)); 42 | } 43 | 44 | @Test 45 | void extendedReadPermissionDoesNotExposeDeleteLink() throws Exception { 46 | WebClient w = rule.createWebClient(); 47 | w.login(READER); 48 | HtmlPage managePage = w.goTo(agentMaintenanceUrl); 49 | assertThat(managePage.querySelector("#" + maintenanceId + " .am__action-delete"), is(nullValue())); 50 | } 51 | 52 | @Test 53 | void configurePermissionDoesExposeDeleteLink() throws Exception { 54 | WebClient w = rule.createWebClient(); 55 | w.login(CONFIGURE); 56 | HtmlPage managePage = w.goTo(agentMaintenanceUrl); 57 | assertThat(managePage.querySelector("#" + maintenanceId + " .am__action-delete"), is(notNullValue())); 58 | } 59 | 60 | @Test 61 | void disconnectPermissionDoesExposeDeleteLink() throws Exception { 62 | WebClient w = rule.createWebClient(); 63 | w.login(DISCONNECT); 64 | HtmlPage managePage = w.goTo(agentMaintenanceUrl); 65 | assertThat(managePage.querySelector("#" + maintenanceId + " .am__action-delete"), is(notNullValue())); 66 | } 67 | 68 | @Test 69 | void extendedReadPermissionCantPost() { 70 | MaintenanceAction action = new MaintenanceAction((SlaveComputer) agent.toComputer()); 71 | try (ACLContext ignored = ACL.as(User.getById(READER, false))) { 72 | assertThrows(AccessDeniedException.class, () -> action.doConfigSubmit(req)); 73 | assertThrows(AccessDeniedException.class, () -> action.doAdd(req)); 74 | assertThat(action.deleteMaintenance(maintenanceId), is(false)); 75 | assertThrows(AccessDeniedException.class, () -> action.deleteMultiple(new String[0])); 76 | } 77 | } 78 | 79 | @Test 80 | void disconnectUserCantEnableDisable() { 81 | MaintenanceAction action = new MaintenanceAction((SlaveComputer) agent.toComputer()); 82 | try (ACLContext ignored = ACL.as(User.getById(DISCONNECT, false))) { 83 | assertThrows(AccessDeniedException.class, () -> action.doDisable(rsp)); 84 | assertThrows(AccessDeniedException.class, () -> action.doEnable(rsp)); 85 | } 86 | } 87 | 88 | @Test 89 | void deleteEnableKeepsOriginalStrategy() throws Exception { 90 | MaintenanceAction action = new MaintenanceAction((SlaveComputer) agent.toComputer()); 91 | try (ACLContext ignored = ACL.as(User.getById(CONFIGURE, false))) { 92 | action.doDisable(rsp); 93 | assertThat(agent.getRetentionStrategy(), instanceOf(Demand.class)); 94 | action.doEnable(rsp); 95 | assertThat(agent.getRetentionStrategy(), instanceOf(AgentMaintenanceRetentionStrategy.class)); 96 | AgentMaintenanceRetentionStrategy strategy = (AgentMaintenanceRetentionStrategy) agent.getRetentionStrategy(); 97 | assertThat(strategy.getRegularRetentionStrategy(), instanceOf(Demand.class)); 98 | Demand demand = (Demand) strategy.getRegularRetentionStrategy(); 99 | assertThat(demand.getIdleDelay(), is(2L)); 100 | assertThat(demand.getInDemandDelay(), is(1L)); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceNodeListenerTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import static com.sap.prd.jenkins.plugins.agent_maintenance.MaintenanceWindow.DATE_FORMATTER; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.instanceOf; 6 | import static org.hamcrest.Matchers.is; 7 | import static org.hamcrest.Matchers.not; 8 | 9 | import hudson.model.Slave; 10 | import hudson.slaves.DumbSlave; 11 | import hudson.slaves.RetentionStrategy; 12 | import hudson.slaves.RetentionStrategy.Always; 13 | import hudson.slaves.RetentionStrategy.Demand; 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.time.LocalDateTime; 17 | import org.junit.jupiter.api.AfterEach; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.api.io.TempDir; 21 | import org.jvnet.hudson.test.JenkinsRule; 22 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 23 | 24 | /** Tests the node listener. */ 25 | @WithJenkins 26 | class MaintenanceNodeListenerTest extends BaseIntegrationTest { 27 | 28 | @TempDir 29 | private File folder; 30 | 31 | private Slave agent; 32 | 33 | /** 34 | * Setup from some tests. 35 | * 36 | * @throws Exception in case of an error 37 | */ 38 | @Override 39 | @BeforeEach 40 | void setup(JenkinsRule rule) throws Exception { 41 | super.setup(rule); 42 | agent = rule.createOnlineSlave(); 43 | AgentMaintenanceRetentionStrategy strategy = 44 | new AgentMaintenanceRetentionStrategy(new Always()); 45 | agent.setRetentionStrategy(strategy); 46 | } 47 | 48 | @AfterEach 49 | void tearDown() throws IOException { 50 | maintenanceHelper.getMaintenanceWindows(agent.getNodeName()).clear(); 51 | } 52 | 53 | @Test 54 | void agentDeleted() throws Exception { 55 | LocalDateTime start = LocalDateTime.now().minusMinutes(1); 56 | LocalDateTime end = start.plusMinutes(5); 57 | MaintenanceWindow mw = 58 | new MaintenanceWindow( 59 | start.format(DATE_FORMATTER), 60 | end.format(DATE_FORMATTER), 61 | "test", 62 | true, 63 | true, 64 | "5", 65 | "test", 66 | null); 67 | maintenanceHelper.addMaintenanceWindow(agent.getNodeName(), mw); 68 | assertThat(agent.toComputer().isAcceptingTasks(), is(false)); 69 | rule.jenkins.removeNode(agent); 70 | assertThat(maintenanceHelper.hasMaintenanceWindows(agent.getNodeName()), is(false)); 71 | } 72 | 73 | @Test 74 | void agentRenamed() throws Exception { 75 | LocalDateTime start = LocalDateTime.now().minusMinutes(1); 76 | LocalDateTime end = start.plusMinutes(10); 77 | MaintenanceWindow mw = 78 | new MaintenanceWindow( 79 | start.format(DATE_FORMATTER), 80 | end.format(DATE_FORMATTER), 81 | "test", 82 | true, 83 | true, 84 | "5", 85 | "test", 86 | null); 87 | maintenanceHelper.addMaintenanceWindow(agent.getNodeName(), mw); 88 | assertThat(agent.toComputer().isAcceptingTasks(), is(false)); 89 | Slave newAgent = 90 | new DumbSlave( 91 | "newAgent", newFolder(folder, "junit").getAbsolutePath(), rule.createComputerLauncher(null)); 92 | rule.jenkins.getNodesObject().replaceNode(agent, newAgent); 93 | assertThat(maintenanceHelper.hasMaintenanceWindows(agent.getNodeName()), is(false)); 94 | assertThat(maintenanceHelper.hasMaintenanceWindows(newAgent.getNodeName()), is(true)); 95 | } 96 | 97 | @Test 98 | void retentionStrategyIsInjected() throws Exception { 99 | MaintenanceConfiguration.getInstance().setInjectRetentionStrategy(true); 100 | agent = rule.createOnlineSlave(); 101 | assertThat(agent.getRetentionStrategy(), instanceOf(AgentMaintenanceRetentionStrategy.class)); 102 | } 103 | 104 | @Test 105 | void retentionStrategyIsInjectedOnRename() throws Exception { 106 | MaintenanceConfiguration.getInstance().setInjectRetentionStrategy(false); 107 | agent = rule.createOnlineSlave(); 108 | assertThat( 109 | agent.getRetentionStrategy(), not(instanceOf(AgentMaintenanceRetentionStrategy.class))); 110 | MaintenanceConfiguration.getInstance().setInjectRetentionStrategy(true); 111 | Slave newAgent = 112 | new DumbSlave( 113 | "newAgent", newFolder(folder, "junit").getAbsolutePath(), rule.createComputerLauncher(null)); 114 | Demand demand = new Demand(1, 1); 115 | newAgent.setRetentionStrategy(demand); 116 | rule.jenkins.getNodesObject().replaceNode(agent, newAgent); 117 | RetentionStrategy strategy = newAgent.getRetentionStrategy(); 118 | assertThat(strategy, instanceOf(AgentMaintenanceRetentionStrategy.class)); 119 | assertThat( 120 | ((AgentMaintenanceRetentionStrategy) strategy).getRegularRetentionStrategy(), is(demand)); 121 | } 122 | 123 | @Test 124 | void retentionStrategyIsNotInjected() throws Exception { 125 | MaintenanceConfiguration.getInstance().setInjectRetentionStrategy(false); 126 | agent = rule.createOnlineSlave(); 127 | assertThat( 128 | agent.getRetentionStrategy(), not(instanceOf(AgentMaintenanceRetentionStrategy.class))); 129 | } 130 | 131 | private static File newFolder(File root, String... subDirs) throws IOException { 132 | String subFolder = String.join("/", subDirs); 133 | File result = new File(root, subFolder); 134 | if (!result.mkdirs()) { 135 | throw new IOException("Couldn't create folders " + root); 136 | } 137 | return result; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Agent Maintenance Plugin for Jenkins 2 | ========================= 3 | 4 | [![Build Status](https://ci.jenkins.io/job/Plugins/job/agent-maintenance-plugin/job/main/badge/icon)](https://ci.jenkins.io/job/Plugins/job/agent-maintenance-plugin/job/main/) 5 | [![Jenkins Plugin](https://img.shields.io/jenkins/plugin/v/agent-maintenance)](https://plugins.jenkins.io/agent-maintenance) 6 | [![GitHub release](https://img.shields.io/github/release/jenkinsci/agent-maintenance-plugin.svg?label=release)](https://github.com/jenkinsci/agent-maintenance-plugin/releases/latest) 7 | [![Jenkins Plugin Installs](https://img.shields.io/jenkins/plugin/i/agent-maintenance.svg?color=blue)](https://plugins.jenkins.io/agent-maintenance) 8 | [![REUSE status](https://api.reuse.software/badge/github.com/jenkinsci/agent-maintenance-plugin)](https://api.reuse.software/info/github.com/jenkinsci/agent-maintenance-plugin) 9 | 10 | 11 | This plugin allows to take agents offline for one or more dedicated time windows on specific dates (e.g. due to a hardware/network maintenance) 12 | while allowing to configure any of the other availability strategies (e.g. *Keep online as much as possible* or *Bring this agent online according to a schedule*) for the times when no maintenance window is active. 13 | 14 | Maintenance activities are usually scheduled on weekends or outside normal business hours during the night. When you have many permanent agents this plugin helps to ensure that your builds are not unexpectedly killed because of a planned network interruption, an OS update or a reboot of the machine. 15 | 16 | 17 | ## Configuration 18 | 19 | On the Jenkins main configuration page you can enable that for newly created (permanent) agents, the agent maintenance availability is automatically injected. 20 | (this does not apply to cloud agents where it doesn't make sense as the agent is deleted right after it was used for a build usually. Not all Cloud implementations inherit from the corresponding classes so that they are detected). 21 | 22 | Using the button *Inject* will inject it to all existing agents when not already configured. The currently configured availability will be set as the regular availability. 23 | This is useful directly after installing the plugin. 24 | 25 | The same way the button *Remove* will remove the agent maintenance availability and restore the configured default availability as the agents availability. 26 | 27 | When creating a new agent select *Take agent offline during maintenance, otherwise use other availability*. 28 | 29 | ![Configuration](/docs/images/configure.PNG) 30 | 31 | To enable for an existing agent open the agent overview page, select *Maintenance Windows* and then click on the *Enable* button. It is possible to also enable it via the configure page but then you need to reconfigure the existing Availability settings. 32 | 33 | ## Defining maintenance windows 34 | To define or delete maintenance windows the user needs the `Computer.CONFIGURE` or `Computer.DISCONNECT` permission. 35 | Use the "x" to directly delete a single maintenance window or use the checkboxes to mark multiple windows and delete with the "Delete selected" button. 36 | Users with the [Computer.ExtendedRead](https://plugins.jenkins.io/extended-read-permission/) permission can see the defined maintenance windows. 37 | 38 | ### Individually for a single agent 39 | Maintenance windows can be defined by opening the corresponding link on the agents overview page. 40 | Using the "Add" button you can directly define a new maintenance window. 41 | Via the "Edit" button one can edit existing maintenance windows (and also add and delete). 42 | 43 | 44 | ### For many agents simultaneously 45 | Going to "Manage Jenkins->Agent Maintenance" will present you a list of all currently defined maintenance windows of all agents. 46 | Using the button "Add" allows to use a label expression to select a list of agents for which to apply the maintenance window. 47 | 48 | ## Recurring maintenance windows 49 | It is also possible to define recurring maintenance windows. Using a cron syntax you can specify the start time of the downtime and a duration. 50 | Recurring maintenance windows are added as planned maintenance window 7 days before they start by default. This way you can easily cancel or modify them before 51 | they start. The lead time for adding recurring maintenance windows can be changed by setting the system property com.sap.prd.jenkins.plugins.agent_maintenance.RecurringMaintenanceWindow.LEAD_TIME_DAYS 52 | during start up of Jenkins. Note that changing the lead time can have unwanted side effects like duplicated/missing maintenance windows. 53 | 54 | Changing a recurring maintenance will not change already scheduled maintenance windows for it. Those need to be adjusted manually. 55 | 56 | ## Best practices 57 | 58 | When defining a maintenance window one has to consider the time it takes for any running build to finish. So if the actual maintenance starts at 8 AM and your builds usually run for 30 minutes you might set the start time to 7:15 AM and define a "Max waiting time in minutes for builds to finish" of 45 minutes. 59 | 60 | At 7:15 the agent will stop accepting new tasks, running builds should have enough time to finish. If a build is still running when the max waiting time is reached an abort request is sent to the build. 61 | 62 | ## Contributing 63 | 64 | Refer to our [contribution guidelines](CONTRIBUTING.md). 65 | 66 | ## License 67 | 68 | Copyright 2022 SAP SE or an SAP affiliate company and agent-maintenance-plugin contributors. Licensed under the [Apache License, Version 2.0](LICENSE). Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/jenkinsci/agent-maintenance-plugin). 69 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/AgentMaintenanceRetentionStrategyTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import static com.sap.prd.jenkins.plugins.agent_maintenance.MaintenanceWindow.DATE_FORMATTER; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.instanceOf; 6 | import static org.hamcrest.Matchers.is; 7 | import static org.hamcrest.Matchers.notNullValue; 8 | import static org.hamcrest.Matchers.nullValue; 9 | 10 | import hudson.model.Slave; 11 | import hudson.slaves.OfflineCause; 12 | import java.time.LocalDateTime; 13 | import java.util.concurrent.TimeUnit; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.Timeout; 16 | 17 | /** Integration tests for the maintenance strategy. */ 18 | class AgentMaintenanceRetentionStrategyTest extends BaseIntegrationTest { 19 | 20 | private void waitForMaintenanceEnd(MaintenanceWindow mw, Slave agent) throws InterruptedException { 21 | while (mw.isMaintenanceScheduled()) { 22 | TimeUnit.SECONDS.sleep(10); 23 | } 24 | triggerCheckCycle(agent); 25 | } 26 | 27 | @Test 28 | @Timeout(600) 29 | void activeMaintenanceWindow() throws Exception { 30 | Slave agent = getAgent("activeMaintenanceWindow"); 31 | LocalDateTime start = LocalDateTime.now().minusMinutes(1); 32 | LocalDateTime end = start.plusMinutes(2); 33 | MaintenanceWindow mw = 34 | new MaintenanceWindow( 35 | start.format(DATE_FORMATTER), 36 | end.format(DATE_FORMATTER), 37 | "test", 38 | true, 39 | true, 40 | "5", 41 | "test", 42 | null); 43 | assertThat(agent.toComputer().isAcceptingTasks(), is(true)); 44 | assertThat(agent.toComputer().isManualLaunchAllowed(), is(true)); 45 | maintenanceHelper.addMaintenanceWindow(agent.getNodeName(), mw); 46 | assertThat(agent.toComputer().isAcceptingTasks(), is(false)); 47 | assertThat(agent.toComputer().isManualLaunchAllowed(), is(false)); 48 | waitForMaintenanceEnd(mw, agent); 49 | assertThat(maintenanceHelper.getMaintenanceWindows(agent.getNodeName()).size(), is(0)); 50 | } 51 | 52 | @Test 53 | @Timeout(600) 54 | void agentGetsDisconnected() throws Exception { 55 | Slave agent = getAgent("agentGetsDisconnected"); 56 | LocalDateTime start = LocalDateTime.now().minusMinutes(1); 57 | LocalDateTime end = start.plusMinutes(15); 58 | MaintenanceWindow mw = 59 | new MaintenanceWindow( 60 | start.format(DATE_FORMATTER), 61 | end.format(DATE_FORMATTER), 62 | "test", 63 | true, 64 | false, 65 | "0", 66 | "test", 67 | null); 68 | String id = mw.getId(); 69 | assertThat(agent.getChannel(), is(notNullValue())); 70 | maintenanceHelper.addMaintenanceWindow(agent.getNodeName(), mw); 71 | assertThat(mw.isMaintenanceScheduled(), is(true)); 72 | waitForDisconnect(agent, mw); 73 | assertThat(agent.getChannel(), is(nullValue())); 74 | maintenanceHelper.deleteMaintenanceWindow(agent.getNodeName(), id); 75 | } 76 | 77 | @Test 78 | @Timeout(600) 79 | void agentComesBackOnline() throws Exception { 80 | String agentName = "agentComesBackOnline"; 81 | Slave agent = getAgent(agentName); 82 | LocalDateTime start = LocalDateTime.now().minusMinutes(1); 83 | // the duration should be sufficiently long, Jenkins is normally calling once 84 | // per minute 85 | // the check method of the retention strategy, but it happens sporadically that 86 | // it takes 2 minutes and then it can happen that the maintenance window is 87 | // already over 88 | // when set to only 4 minutes 89 | LocalDateTime end = start.plusMinutes(15); 90 | MaintenanceWindow mw = 91 | new MaintenanceWindow( 92 | start.format(DATE_FORMATTER), 93 | end.format(DATE_FORMATTER), 94 | "test", 95 | true, 96 | false, 97 | "0", 98 | "test", 99 | null); 100 | assertThat(agent.toComputer().isOnline(), is(true)); 101 | maintenanceHelper.addMaintenanceWindow(agentName, mw); 102 | waitForDisconnect(agent, mw); 103 | // Instead of waiting for the maintenance window to be over just delete it 104 | maintenanceHelper.deleteMaintenanceWindow(agentName, mw.getId()); 105 | triggerCheckCycle(agent); 106 | while (!agent.toComputer().isOnline()) { 107 | TimeUnit.SECONDS.sleep(10); 108 | } 109 | assertThat(agent.toComputer().isOnline(), is(true)); 110 | assertThat(maintenanceHelper.hasMaintenanceWindows(agentName), is(false)); 111 | } 112 | 113 | @Test 114 | @Timeout(600) 115 | void agentStaysOffline() throws Exception { 116 | String agentName = "agentStaysOffline"; 117 | Slave agent = getAgent(agentName); 118 | LocalDateTime start = LocalDateTime.now().minusMinutes(1); 119 | LocalDateTime end = start.plusMinutes(15); 120 | MaintenanceWindow mw = 121 | new MaintenanceWindow( 122 | start.format(DATE_FORMATTER), 123 | end.format(DATE_FORMATTER), 124 | "test", 125 | false, 126 | false, 127 | "0", 128 | "test", 129 | null); 130 | maintenanceHelper.addMaintenanceWindow(agentName, mw); 131 | waitForDisconnect(agent, mw); 132 | // Instead of waiting for the maintenance window to be over just delete it 133 | maintenanceHelper.deleteMaintenanceWindow(agentName, mw.getId()); 134 | triggerCheckCycle(agent); 135 | TimeUnit.MINUTES.sleep(1); 136 | assertThat(agent.getChannel(), is(nullValue())); 137 | OfflineCause oc = agent.toComputer().getOfflineCause(); 138 | assertThat(oc, instanceOf(MaintenanceOfflineCause.class)); 139 | assertThat(((MaintenanceOfflineCause) oc).isTakeOnline(), is(false)); 140 | assertThat(maintenanceHelper.hasMaintenanceWindows(agentName), is(false)); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/test/java/com/sap/prd/jenkins/plugins/agent_maintenance/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.prd.jenkins.plugins.agent_maintenance; 2 | 3 | import static com.sap.prd.jenkins.plugins.agent_maintenance.MaintenanceWindow.DATE_FORMATTER; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.instanceOf; 6 | import static org.hamcrest.Matchers.is; 7 | import static org.hamcrest.Matchers.lessThan; 8 | import static org.hamcrest.Matchers.notNullValue; 9 | import static org.hamcrest.Matchers.nullValue; 10 | 11 | import hudson.model.FreeStyleBuild; 12 | import hudson.model.FreeStyleProject; 13 | import hudson.model.Result; 14 | import hudson.model.Slave; 15 | import hudson.slaves.RetentionStrategy.Demand; 16 | import java.time.LocalDateTime; 17 | import java.util.concurrent.TimeUnit; 18 | import jenkins.model.InterruptedBuildAction; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.api.Timeout; 21 | import org.jvnet.hudson.test.SleepBuilder; 22 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 23 | 24 | /** 25 | * Tests retention strategy that involves running jobs. 26 | */ 27 | @WithJenkins 28 | class IntegrationTest extends BaseIntegrationTest { 29 | 30 | @Test 31 | @Timeout(300) 32 | void waitForRunningProjectToFinishBeforeDisconnect() throws Exception { 33 | Slave agent = getAgent("waitForRunningProjectToFinishBeforeDisconnect"); 34 | FreeStyleProject project = rule.createFreeStyleProject(); 35 | project.setAssignedLabel(agent.getSelfLabel()); 36 | LocalDateTime start = LocalDateTime.now(); 37 | LocalDateTime end = start.plusMinutes(10); 38 | MaintenanceWindow mw = 39 | new MaintenanceWindow( 40 | start.format(DATE_FORMATTER), 41 | end.format(DATE_FORMATTER), 42 | "test", 43 | true, 44 | true, 45 | "3", 46 | "test", 47 | null); 48 | String id = mw.getId(); 49 | project.getBuildersList().add(new SleepBuilder(1000 * 60 * 2)); 50 | FreeStyleBuild build = project.scheduleBuild2(0).waitForStart(); 51 | maintenanceHelper.addMaintenanceWindow(agent.getNodeName(), mw); 52 | assertThat(agent.toComputer().isAcceptingTasks(), is(false)); 53 | waitForDisconnect(agent, mw); 54 | assertThat(build.getResult(), is(Result.SUCCESS)); 55 | maintenanceHelper.deleteMaintenanceWindow(agent.getNodeName(), id); 56 | } 57 | 58 | @Test 59 | @Timeout(300) 60 | void projectGetsAbortedWhenRunningTooLong() throws Exception { 61 | Slave agent = getAgent("projectGetsAbortedWhenRunningTooLong"); 62 | FreeStyleProject project = rule.createFreeStyleProject(); 63 | project.setAssignedLabel(agent.getSelfLabel()); 64 | LocalDateTime start = LocalDateTime.now(); 65 | LocalDateTime end = start.plusMinutes(10); 66 | MaintenanceWindow mw = 67 | new MaintenanceWindow( 68 | start.format(DATE_FORMATTER), 69 | end.format(DATE_FORMATTER), 70 | "test", 71 | true, 72 | true, 73 | "1", 74 | "test", 75 | null); 76 | String id = mw.getId(); 77 | project.getBuildersList().add(new SleepBuilder(1000 * 60 * 7)); 78 | FreeStyleBuild build = project.scheduleBuild2(0).waitForStart(); 79 | maintenanceHelper.addMaintenanceWindow(agent.getNodeName(), mw); 80 | assertThat(agent.toComputer().isAcceptingTasks(), is(false)); 81 | waitForDisconnect(agent, mw); 82 | assertThat(build.getResult(), is(Result.ABORTED)); 83 | InterruptedBuildAction interruptedCauseAction = build.getAction(InterruptedBuildAction.class); 84 | assertThat(interruptedCauseAction, is(notNullValue())); 85 | assertThat(interruptedCauseAction.getCauses().get(0), instanceOf(MaintenanceInterruption.class)); 86 | maintenanceHelper.deleteMaintenanceWindow(agent.getNodeName(), id); 87 | } 88 | 89 | @Test 90 | @Timeout(300) 91 | void projectGetsAbortedWithoutKeepOnline() throws Exception { 92 | Slave agent = getAgent("projectGetsAbortedWhenRunningTooLong"); 93 | FreeStyleProject project = rule.createFreeStyleProject(); 94 | project.setAssignedLabel(agent.getSelfLabel()); 95 | LocalDateTime start = LocalDateTime.now(); 96 | LocalDateTime end = start.plusMinutes(10); 97 | MaintenanceWindow mw = 98 | new MaintenanceWindow( 99 | start.format(DATE_FORMATTER), 100 | end.format(DATE_FORMATTER), 101 | "test", 102 | true, 103 | false, 104 | "5", 105 | "test", 106 | null); 107 | String id = mw.getId(); 108 | project.getBuildersList().add(new SleepBuilder(1000 * 60 * 7)); 109 | FreeStyleBuild build = project.scheduleBuild2(0).waitForStart(); 110 | maintenanceHelper.addMaintenanceWindow(agent.getNodeName(), mw); 111 | assertThat(agent.toComputer().isAcceptingTasks(), is(false)); 112 | waitForDisconnect(agent, mw); 113 | assertThat(build.getResult(), is(Result.ABORTED)); 114 | assertThat(build.getDuration(), lessThan(1000L * 60 * 3)); 115 | maintenanceHelper.deleteMaintenanceWindow(agent.getNodeName(), id); 116 | } 117 | 118 | @Test 119 | @Timeout(300) 120 | void onDemandStrategyIsAppliedProperly() throws Exception { 121 | Demand demandStrategy = new Demand(1, 1); 122 | Slave agent = getAgent("onDemandStrategyIsAppliedProperly", demandStrategy); 123 | FreeStyleProject project = rule.createFreeStyleProject(); 124 | project.setAssignedLabel(agent.getSelfLabel()); 125 | project.getBuildersList().add(new SleepBuilder(1000)); 126 | assertThat(agent.toComputer().isAcceptingTasks(), is(true)); 127 | waitForDisconnect(agent, null); 128 | project.scheduleBuild2(0); 129 | waitForConnect(agent); 130 | waitForDisconnect(agent, null); 131 | assertThat(agent.getChannel(), is(nullValue())); 132 | } 133 | 134 | protected void waitForConnect(Slave agent) throws Exception { 135 | while (agent.getChannel() == null) { 136 | TimeUnit.SECONDS.sleep(10); 137 | triggerCheckCycle(agent); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/resources/com/sap/prd/jenkins/plugins/agent_maintenance/MaintenanceLink/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 |