Remote Control

Remote Control allows you to easily execute code on other Atlassian applications with ScriptRunner installed.

You could do the same by writing and deploying remote APIs, eg REST endpoints. However, this method allows you to run arbitrary code on any instance, without deploying it first. You can execute code locally, pass a value to a remote instance, manipulate it, and return some new value to your local instance.

If you have many instances of an application you can easily run code across all the instances - possible examples might be:

  • Check if any projects are anonymously accessible

  • Find the version of a common plugin

  • Anything you could do with remote events, e.g. mark a Jira version as released when a tag is created in Bitbucket

Example

This script gets the number of issues in both the local instance, and the "applinked" instance, and returns a message to the console.

import com.atlassian.jira.component.ComponentAccessor
import com.onresolve.scriptrunner.remote.RemoteControl

def issuesInRemote = RemoteControl.forPrimaryJiraAppLink().exec {
    ComponentAccessor.issueManager.issueCount
}

def issuesInLocal = ComponentAccessor.issueManager.issueCount

"There are ${issuesInRemote} issues in the remote instance, ${issuesInLocal} in the local instance"

As you can see it’s as easy to write a script to operate on a remote instance as it is on a local instance.

Security

Executing code on the remote application requires administrator privileges on the target application, in the same way as executing custom code or a built-in script requires administrator permissions. If you are running code as the current user, you may need to switch user before executing the remote closure code.

If you cannot or do not want to switch to an admin user, you can call a REST endpoint rather than using remote control, where you can set the security as you choose.

Under the Hood

Under the covers, we serialize the closure you pass to the exec method, and any variables it uses. This is sent to the remote machine in binary form, then executed. Any results are sent back to the calling instance.

When you use code like .each { …​ } the part in the brackets is a closure.

This means, that any variables passed in or back from the closure must be serializable. In practice, if you stick to passing simple types like strings, numbers, dates, or Collections (Lists, Maps etc) of these types it will work fine.

If you don’t read any further, just pass around strings, numbers, dates, etc or Collections thereof.

If you attempt to pass something not simple, e.g. a Jira project object you will get an exception:

import com.atlassian.jira.component.ComponentAccessor
import com.onresolve.scriptrunner.remote.RemoteControl

def project = ComponentAccessor.projectManager.getProjectObjByKey("JRA")

RemoteControl.forPrimaryJiraAppLink().exec {
    log.debug project.versions
}

results in an exception: Caused by: java.io.NotSerializableException: com.atlassian.jira.project.ProjectImpl.

This applies equally to returning objects from the closure. So, rather than returning a Collection of Version objects we return just their names:

import com.atlassian.jira.component.ComponentAccessor
import com.onresolve.scriptrunner.remote.RemoteControl

def projectKey = "JRA"

def remoteVersions = RemoteControl.forPrimaryJiraAppLink().exec {
    def project = ComponentAccessor.projectManager.getProjectObjByKey(projectKey)
    project.versions.collect { it.name }
}

"Versions for project: $projectKey on the remote are: ${remoteVersions}"

Similarly, objects like ProjectManager etc. cannot be serialized and passed to the remote (or back), so you need to retrieve it from the ComponentAccessor (or ComponentLocator on Bitbucket and Confluence) inside the closure.

Optional Returns

You may recall that the return keyword is optional in groovy…​ if omitted the result of the last statement executed in a closure or method will be returned.

This can cause inadvertent problems with remote control - for example, imagine a case where you are creating an issue on a remote instance:

RemoteControl.forPrimaryJiraAppLink().exec {
    def issueManager = ComponentAccessor.getComponent(IssueManager)
    issueManager.createIssueObject("admin", [
        summary: "a new issue",
        // etc
    ])
}

Because createIssueObject returns an Issue, and this statement is the last in the closure, the system attempts to return the Issue object from the remote application. This will fail because Issue is not serializable: io.remotecontrol.client.UnserializableReturnException: The return value of the command was not serializable, its string representation was 'JRA-13'


Solutions might be just returning the issue key, or, if you don’t care about the return value, return nothing:

RemoteControl.forPrimaryJiraAppLink().exec {
    def issueManager = ComponentAccessor.getComponent(IssueManager)
    issueManager.createIssueObject("admin", [
        summary: "a new issue",
        // etc
    ])
    null
}

Running Code on Multiple Instances

By iterating through the application links of a particular application type, you can execute code on multiple instances sequentially:

import com.atlassian.applinks.api.ApplicationLinkService
import com.atlassian.applinks.api.application.jira.JiraApplicationType
import com.atlassian.jira.component.ComponentAccessor
import com.onresolve.scriptrunner.remote.RemoteControl

def applicationLinkService = ComponentAccessor.getComponent(ApplicationLinkService)
def jiraAppLinks = applicationLinkService.getApplicationLinks(JiraApplicationType) // <1>

def issueTotals = jiraAppLinks.collect { appLink -> // <2>
    RemoteControl.forAppLink(appLink).exec {
        ComponentAccessor.issueManager.issueCount
    }
}

"Issue totals are: ${issueTotals.join(", ")}, total: ${issueTotals.sum()}."

Line 7: Get all Jira-type application links

Line 9: collect the results into a list

Running Code on Unlinked Instances

You can also execute code on instances without an application link, so long as you can authenticate as an administrator. Example:

import com.atlassian.jira.component.ComponentAccessor
import com.onresolve.scriptrunner.remote.RemoteControl

def currentUser = RemoteControl.forUrlWithBasicAuth("https://jira.acme.com", "admin", "secret").exec {
    ComponentAccessor.jiraAuthenticationContext.getLoggedInUser()?.name
}

"Logged in to the remote as ${currentUser}"

Running Code on Different Products

So far all the examples have focused on running code an another Jira instance, or instances, from an existing Jira. This is relatively easy, and the same approach works for all supported products.

What if you needed to run a script on Bitbucket from a Jira instance? For example, you may wish to find all projects that don’t have a matching Bitbucket project (by key), or check that no users are permissioned in a Bitbucket repository except the developers role for the corresponding project.

In this case you need to use an API for an application that is not native - the problem is the Bitbucket API is not available from Jira. Whilst you could do it all with reflection this is quite tricky.

What we do here is use Grape to grab the relevant API dependency. This will take a few seconds to download the relevant jar file, but that delay is only the first time the script is run.

Using the Bitbucket API from Jira:

import com.atlassian.bitbucket.project.ProjectService
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.sal.api.component.ComponentLocator
import com.onresolve.scriptrunner.remote.RemoteControl

@Grapes([ // <1>
    @Grab("com.atlassian.bitbucket.server:bitbucket-api:5.1.0"),
    @GrabExclude("com.atlassian.annotations:atlassian-annotations")
])
def dummy = 1 // <2>

def jraProjectKeys = ComponentAccessor.projectManager.projectObjects*.key

def bbsProjKeys = RemoteControl.forPrimaryBitbucketAppLink().exec {
    def projectService = ComponentLocator.getComponent(ProjectService)
    projectService.findAllKeys()
}

(jraProjectKeys - bbsProjKeys).each {
    log.warn("No Bitbucket project for JIRA project: $it")
    // create it...
}

(bbsProjKeys - jraProjectKeys).each {
    log.warn("No JIRA project for Bitbucket project: $it")
    // create it...
}

Line 6: Grab the Bitbucket API dependency.

Line 10: A dummy variable, as annotations need to annotate "something."

If you have problems or you find this too difficult, use could use remote events, or write a REST endpoint in Bitbucket and call it from Jira.

Synchronizing Worklogs Example

Internally, we use several Jira instances. We do our support and development on one instance, but have to log our time on another instance. We don’t want to duplicate issues across as this causes confusion, just on the time-tracking instance we just have several bucket tickets for the combination of products and whether it’s development, maintenance or support.

The problem with this is that the development leads cannot drill down to issues where the time spent. Our solution uses an event listener or worklog created/updated/deleted, and remote control to create or update the worklog on the target ticket.

The event listener looks like:

The script can be found here.

As in the example above, the .exec() method can take multiple closures, not just one. If used like this the returned value(s) of each closure are passed to the next one. This is a good way of reusing code, as with remote control you cannot call a method defined in the script, outside of the closure.



On this page