jenkinsci / lockable-resources-plugin

Lock resources against concurrent use
https://plugins.jenkins.io/lockable-resources
MIT License
87 stars 183 forks source link

Add ability to "offline" a resource through Declarative pipeline code #689

Open asadniazi opened 1 month ago

asadniazi commented 1 month ago

What feature do you want to see added?

Currently there is no easy way to offline/reserve/unreserve a resource if it crashes through the Groovy declarative pipeline code. The subsequent builds try to use the same resource and fail. Someone has to reserve that resource manually through UI so that the other jobs do not pick the same resource. This maybe possible using the Java libraries but it is not easy to do. There should be an out of the box step like the "lock" stop to reserve, unreserve, offline resource in the declarative pipeline code.

Upstream changes

No response

Are you interested in contributing this feature?

PayBas commented 1 month ago

This is not natively supported, but since I required the same functionality, I made a Jenkins shared library (https://www.jenkins.io/doc/book/pipeline/shared-libraries/) method for this:

/**
 * params:
 *     action       - method to call
 *     resourceName - name of lockable resource
 *     textParam    - text-string to pass to set ReservedBy, Note or Steal text
 */
def String call(String action, String resourceName, String textParam = "") {

    if (resourceName == null || !resourceName.trim()) {
        echo "WARNING: no or empty lockable resource name provided! Aborting"
        return ""
    }
    def lockManager = org.jenkins.plugins.lockableresources.LockableResourcesManager.get()
    def lockableResource = lockManager.fromName(resourceName)
    def retVal = ""

    if (lockableResource != null && !lockableResource.isEphemeral()) {
        switch(action) {
            case "getReservedBy":
                def reservedBy = lockableResource.getReservedBy()
                if (reservedBy != null && reservedBy) {
                    retVal = lockableResource.getReservedBy()
                    echo "Resource: ${resourceName}, ReservedBy: ${retVal}"
                } else {
                    echo "Lockable resource: [${resourceName}] currently not reserved."
                }
                break
            case "setReservedBy":
                if (lockManager.reserve([lockableResource], textParam)) {
                    echo "Resource: ${resourceName}, ReservedBy: ${textParam}"
                } else {
                    echo "Lockable resource: [${resourceName}] could not be reserved by: '${textParam}'"
                }
                break
            case "unreserve":
                lockManager.unreserve([lockableResource])
                break
            case "steal":
                lockManager.steal([lockableResource], textParam)
                break
            case "getNote":
                retVal = lockableResource.getNote()
                echo "Resource: ${resourceName}, Note: ${retVal}"
                break
            case "setNote":
                retVal = lockableResource.setNote(textParam)
                break
            case "isLocked":
                retVal = lockableResource.isLocked()
                echo "Resource: ${resourceName}, Locked: ${retVal}"
                break
            case "quarantine":
                // Checks if the resource is already reserved, if not steal the lock
                def reservedBy = lockableResource.getReservedBy()
                if (reservedBy == null) {
                    echo "Reserving lockable resource [${resourceName}] for troubleshooting and manual repair"
                    lockableResource.setNote("FAILED build: <a href='${BUILD_URL}' target='_blank'>${JOB_BASE_NAME} : ${BUILD_DISPLAY_NAME}</a>")
                    lockManager.steal([lockableResource], textParam ?: 'Jenkins auto-quarantine')
                } else {
                    echo "Lockable Resource: [${resourceName}] has already been reserved by: '${reservedBy}'"
                }
                break
            default:
                echo "Unsupported action requested!"
        }
    } else {
         echo "WARNING: Lockable resource [${resourceName}] not found or ephemeral!"
    }
    return retVal
}

Which can be called from a declarative pipeline using:

#!/usr/bin/env groovy
pipeline {
  agent none
  options {
    lock(resource: 'some-resource', variable: 'RESOURCE')
  }
  stages {
    stage('Do stuff') {
      agent any
      steps {
        echo "Hi there!"
        error("Force an error")
      }
      post {
        failure {
          script {
            manageLockableResources("quarantine", env.RESOURCE, currentBuild.getBuildCauses('hudson.model.Cause$UserIdCause')[0]?.userName ?: 'Jenkins')
          }
        }
      }
    }
  }
}

Note that you might have to tweak this pipeline a bit. I had to remove 99% in order to share this, so I haven't tested it.

asadniazi commented 1 month ago

@PayBas Cool, this looks good. I'll give it a try once I get a chance and confirm if it works for me. Thanks!

mPokornyETM commented 3 days ago

This will not works when you have a big amount of resources or many acces to it. Because you can not synchronized the groovy && java operatiosn and you will raise concurent modification exception. The best way is to extend LRM for 'missing' methods.