Uploaded image for project: 'Jenkins'
  1. Jenkins
  2. JENKINS-27927

Some interface idioms do not work in Groovy CPS

    XMLWordPrintable

Details

    Description

      Since class inheritance is not available to groovy workflow scripts, I thought I'd try some alternatives, so I gave "interfaces" a go instead. I want to specify an interface that provides the templating for a common behavior I want to achieve.

      e.g. MyFoo.groovy could implement an interface specified in Foo.groovy. Other scripts e.g. Bar.groovy could then invoke a common interface specified in YourFoo.groovy, MyFoo.groovy, RonaldsFoo.groovy and so on.

      In trying out a very simple example of an interface to achieve what I want, the groovy-cps compiler errors.

      Here is the valid groovy code that I tried (and works) under a groovy console:

      interface Foo {
      String name()
      }
      
      
      b = [name: {return "Ronald"}]
      a = b as Foo
      
      println a.name()
      
      return
      

      This prints "Ronald" as you'd expect.

      The code that I tried in an example workflow, I was hoping could do the same, but does not:

      node {
          b = [name: {return "Ronald"}]
          a = b as Foo
      
          echo a.name()
      }
      
      interface Foo {
      String name()
      }
      

      The error that is thrown when I hit "Apply" after entering the code in the Groovy CPS DSL box is:

      javax.servlet.ServletException: java.lang.NullPointerException: Cannot invoke method visit() on null object
      	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:796)
      	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:876)
      	at org.kohsuke.stapler.MetaClass$6.doDispatch(MetaClass.java:249)
      	at org.kohsuke.stapler.NameBasedDispatcher.dispatch(NameBasedDispatcher.java:53)
      	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:746)
      	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:876)
      	at org.kohsuke.stapler.MetaClass$6.doDispatch(MetaClass.java:249)
      	at org.kohsuke.stapler.NameBasedDispatcher.dispatch(NameBasedDispatcher.java:53)
      	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:746)
      	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:876)
      	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:649)
      	at org.kohsuke.stapler.Stapler.service(Stapler.java:238)
      	at javax.servlet.http.HttpServlet.service(HttpServlet.java:848)
      	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:686)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1494)
      	at hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:123)
      	at hudson.plugins.greenballs.GreenBallFilter.doFilter(GreenBallFilter.java:58)
      	at hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:120)
      	at hudson.util.PluginServletFilter.doFilter(PluginServletFilter.java:114)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
      	at hudson.security.csrf.CrumbFilter.doFilter(CrumbFilter.java:48)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
      	at hudson.security.ChainedServletFilter$1.doFilter(ChainedServletFilter.java:84)
      	at hudson.security.ChainedServletFilter.doFilter(ChainedServletFilter.java:76)
      	at hudson.security.HudsonFilter.doFilter(HudsonFilter.java:168)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
      	at org.kohsuke.stapler.compression.CompressionFilter.doFilter(CompressionFilter.java:49)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
      	at hudson.util.CharacterEncodingFilter.doFilter(CharacterEncodingFilter.java:81)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
      	at org.kohsuke.stapler.DiagnosticThreadNameFilter.doFilter(DiagnosticThreadNameFilter.java:30)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1474)
      	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:499)
      	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:137)
      	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:533)
      	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:231)
      	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1086)
      	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:428)
      	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:193)
      	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1020)
      	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135)
      	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116)
      	at org.eclipse.jetty.server.Server.handle(Server.java:370)
      	at org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:489)
      	at org.eclipse.jetty.server.AbstractHttpConnection.content(AbstractHttpConnection.java:960)
      	at org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.content(AbstractHttpConnection.java:1021)
      	at org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:865)
      	at org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:240)
      	at org.eclipse.jetty.server.AsyncHttpConnection.handle(AsyncHttpConnection.java:82)
      	at org.eclipse.jetty.io.nio.SelectChannelEndPoint.handle(SelectChannelEndPoint.java:668)
      	at org.eclipse.jetty.io.nio.SelectChannelEndPoint$1.run(SelectChannelEndPoint.java:52)
      	at winstone.BoundedExecutorService$1.run(BoundedExecutorService.java:77)
      	at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
      	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
      	at java.lang.Thread.run(Thread.java:619)
      Caused by: java.lang.NullPointerException: Cannot invoke method visit() on null object
      	at org.codehaus.groovy.runtime.NullObject.invokeMethod(NullObject.java:77)
      	at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:45)
      	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:42)
      	at org.codehaus.groovy.runtime.callsite.NullCallSite.call(NullCallSite.java:32)
      	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:42)
      	at org.codehaus.groovy.ast.stmt.BlockStatement$visit$0.call(Unknown Source)
      	at com.cloudbees.groovy.cps.CpsTransformer.visitMethod(CpsTransformer.groovy:159)
      	at sun.reflect.GeneratedMethodAccessor104.invoke(Unknown Source)
      	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
      	at java.lang.reflect.Method.invoke(Method.java:597)
      	at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:90)
      	at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:233)
      	at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:361)
      	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:903)
      	at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.callCurrent(PogoMetaClassSite.java:66)
      	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:141)
      	at com.cloudbees.groovy.cps.CpsTransformer$_call_closure1.doCall(CpsTransformer.groovy:88)
      	at sun.reflect.GeneratedMethodAccessor1139.invoke(Unknown Source)
      	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
      	at java.lang.reflect.Method.invoke(Method.java:597)
      	at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:90)
      	at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:233)
      	at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:272)
      	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:903)
      	at groovy.lang.Closure.call(Closure.java:415)
      	at groovy.lang.Closure.call(Closure.java:428)
      	at org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:1379)
      	at org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:1351)
      	at org.codehaus.groovy.runtime.dgm$170.invoke(Unknown Source)
      	at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite$PojoMetaMethodSiteNoUnwrapNoCoerce.invoke(PojoMetaMethodSite.java:271)
      	at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite.call(PojoMetaMethodSite.java:53)
      	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:116)
      	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callSafe(AbstractCallSite.java:82)
      	at com.cloudbees.groovy.cps.CpsTransformer.call(CpsTransformer.groovy:88)
      	at org.codehaus.groovy.control.CompilationUnit.applyToPrimaryClassNodes(CompilationUnit.java:970)
      	at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:548)
      	at org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:526)
      	at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:503)
      	at groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:302)
      	at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:281)
      	at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:267)
      	at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:214)
      	at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:224)
      	at org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition$DescriptorImpl.doCheckScript(CpsFlowDefinition.java:123)
      	at sun.reflect.GeneratedMethodAccessor451.invoke(Unknown Source)
      	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
      	at java.lang.reflect.Method.invoke(Method.java:597)
      	at org.kohsuke.stapler.Function$InstanceFunction.invoke(Function.java:298)
      	at org.kohsuke.stapler.Function.bindAndInvoke(Function.java:161)
      	at org.kohsuke.stapler.Function.bindAndInvokeAndServeResponse(Function.java:96)
      	at org.kohsuke.stapler.MetaClass$1.doDispatch(MetaClass.java:121)
      	at org.kohsuke.stapler.NameBasedDispatcher.dispatch(NameBasedDispatcher.java:53)
      	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:746)
      	... 54 more
      

      So although this support for groovy interfaces does not appear to be supported - is it worthy of consideration?

      Attachments

        Issue Links

          Activity

            tomjdalton Thomas Dalton created issue -
            jglick Jesse Glick added a comment -

            Yet another exotic Groovy syntax which is apparently not yet supported by groovy-cps. I would suggest sticking to more conservative Java-like idioms which are more likely to work.

            jglick Jesse Glick added a comment - Yet another exotic Groovy syntax which is apparently not yet supported by groovy-cps . I would suggest sticking to more conservative Java-like idioms which are more likely to work.
            jglick Jesse Glick made changes -
            Field Original Value New Value
            Assignee Jesse Glick [ jglick ] Kohsuke Kawaguchi [ kohsuke ]
            Issue Type New Feature [ 2 ] Bug [ 1 ]
            Summary Use of "interface" in workflow scripts wont compile Some interface idioms do not work in Groovy CPS
            tomjdalton Thomas Dalton added a comment -

            Hi Jesse,

            Thanks for getting back to me again.

            The reason I'm looking at the workflow-plugin is because it allows a programmatic interface, and it is potentially a very powerful means to share jenkins job setups across multiple teams and engineers, and even more so if it can work in a way that provides the extensibility and reuse that the groovy language can offer. This means things like class inheritance, interfaces and other OO techniques are desirable and of particular interest to me. Moreover, the flows I'm looking at are inherently quite complex and I would ideally like to generate infra for them that could be used by a number of people/teams - not just copy/pasted.

            If I go with conservative, I fear I will end up with multiple copies of the same rehashed code for each instance where it is actually used - and that means more places for it to go wrong and maintain.

            So what would really set the workflow-plugin apart from e.g. a typical multijob setup is the ability to do some of the less "conservative", more "groovy" things

            Regards,
            Tom.

            tomjdalton Thomas Dalton added a comment - Hi Jesse, Thanks for getting back to me again. The reason I'm looking at the workflow-plugin is because it allows a programmatic interface, and it is potentially a very powerful means to share jenkins job setups across multiple teams and engineers, and even more so if it can work in a way that provides the extensibility and reuse that the groovy language can offer. This means things like class inheritance, interfaces and other OO techniques are desirable and of particular interest to me. Moreover, the flows I'm looking at are inherently quite complex and I would ideally like to generate infra for them that could be used by a number of people/teams - not just copy/pasted. If I go with conservative, I fear I will end up with multiple copies of the same rehashed code for each instance where it is actually used - and that means more places for it to go wrong and maintain. So what would really set the workflow-plugin apart from e.g. a typical multijob setup is the ability to do some of the less "conservative", more "groovy" things Regards, Tom.
            jglick Jesse Glick added a comment -

            Sure, the intention is to support all Groovy language features, but there is a way to go before we get there.

            jglick Jesse Glick added a comment - Sure, the intention is to support all Groovy language features, but there is a way to go before we get there.
            tomjdalton Thomas Dalton added a comment -

            I'm glad the intention is aligned with the desire to have these groovy language features available and understand that support for interface will require work in the groovy CPS.

            In the short term though, is there a recommended workaround that could provide some interface like equivalent? Or is it just not possible to do anything like this at this stage?
            Perhaps pass HashMaps of Closures between the top level "classes" as per
            https://github.com/jenkinsci/workflow-plugin/blob/master/cps-global-lib/README.md#writing-shared-code

            e.g.

            org/foo/Zot.groovy
            package org.foo
            // Closures define the interface for Zot
            Closure show
            Closure name
            
            def specify_interface (Hashmap hm) {
                this.show = hm["show"]
                this.name = hm["name"]
            }
            
            
            flow.groovy
            node {
                def z = new org.foo.Zot()
                z.specify_interface(["show": { echo "Hello" }, "name": { return "Bernard" }])
            
                // you can then pass z around beyond the scope of flow.groovy
                z.show()
                echo z.name()
            }
            

            Or is there a more elegant way within the constraints of what is and what is not possible today?

            tomjdalton Thomas Dalton added a comment - I'm glad the intention is aligned with the desire to have these groovy language features available and understand that support for interface will require work in the groovy CPS. In the short term though, is there a recommended workaround that could provide some interface like equivalent? Or is it just not possible to do anything like this at this stage? Perhaps pass HashMaps of Closures between the top level "classes" as per https://github.com/jenkinsci/workflow-plugin/blob/master/cps-global-lib/README.md#writing-shared-code e.g. org/foo/Zot.groovy package org.foo // Closures define the interface for Zot Closure show Closure name def specify_interface (Hashmap hm) { this .show = hm[ "show" ] this .name = hm[ "name" ] } flow.groovy node { def z = new org.foo.Zot() z.specify_interface([ "show" : { echo "Hello" }, "name" : { return "Bernard" }]) // you can then pass z around beyond the scope of flow.groovy z.show() echo z.name() } Or is there a more elegant way within the constraints of what is and what is not possible today?
            jglick Jesse Glick added a comment -

            I would just try using Java-compatible syntax first. More likely to already be supported.

            jglick Jesse Glick added a comment - I would just try using Java-compatible syntax first. More likely to already be supported.
            tomjdalton Thomas Dalton added a comment -

            Ahh.. in which case I apologies for the miscommunication on my part, I actually tried the Java syntax first - I then reduced the problem to simpler forms to post the issue.

            Here is what I tried originally:

            interface Foo {
                String name()
            }
            class Bar implements Foo {
                def String name() {
                    return "Ronald"
                }
            }
            
            node {
                b = new Bar()
                a = b as Foo
            
                echo a.name()
            }
            

            This doesn't work either and the same exception Cannot invoke method visit() on null object is thrown.

            Yet under a groovy console:

            interface Foo {
                String name()
            }
            
            class Bar implements Foo {
                def String name() {
                    return "Ronald"
                }
            }
            
            b = new Bar()
            a = b as Foo
            
            println a.name()
            

            It works fine. Hence I think it is likely a problem with all uses of interface and why I'm on the lookout for a decent workaround

            tomjdalton Thomas Dalton added a comment - Ahh.. in which case I apologies for the miscommunication on my part, I actually tried the Java syntax first - I then reduced the problem to simpler forms to post the issue. Here is what I tried originally: interface Foo { String name() } class Bar implements Foo { def String name() { return "Ronald" } } node { b = new Bar() a = b as Foo echo a.name() } This doesn't work either and the same exception Cannot invoke method visit() on null object is thrown. Yet under a groovy console: interface Foo { String name() } class Bar implements Foo { def String name() { return "Ronald" } } b = new Bar() a = b as Foo println a.name() It works fine. Hence I think it is likely a problem with all uses of interface and why I'm on the lookout for a decent workaround
            jglick Jesse Glick added a comment -

            Well my first suggestion is to try actual Java syntax:

            Foo a = b;
            

            in case it is the as keyword which causes the problem.

            jglick Jesse Glick added a comment - Well my first suggestion is to try actual Java syntax: Foo a = b; in case it is the as keyword which causes the problem.
            tomjdalton Thomas Dalton added a comment -

            If I replace

            a = b as Foo
            

            with

            Foo a = b
            

            It doesn't work either I'm afraid , same exception.

            tomjdalton Thomas Dalton added a comment - If I replace a = b as Foo with Foo a = b It doesn't work either I'm afraid , same exception.
            jglick Jesse Glick made changes -
            Link This issue is blocking JENKINS-25979 [ JENKINS-25979 ]

            Like Thomas I too wanted to leverage interfaces in the workflow plugin for a more adaptable workflow to share amongst teams. I get the exact same error as is listed above.

            java.lang.NullPointerException: Cannot invoke method visit() on null object

            I'm really hoping for a fix soon. As this would be a power way to create a sharable workflow design with robust features. I'm envisioning a OOD aspect to a Jenkins Pipeline Job.

            Is fixing the above error and allowing groovy interfaces within the Jenkins Pipeline something that is on the current schedule? Is so, what is the timeline for this being added??

            cmertz Christopher Mertz added a comment - Like Thomas I too wanted to leverage interfaces in the workflow plugin for a more adaptable workflow to share amongst teams. I get the exact same error as is listed above. java.lang.NullPointerException: Cannot invoke method visit() on null object I'm really hoping for a fix soon. As this would be a power way to create a sharable workflow design with robust features. I'm envisioning a OOD aspect to a Jenkins Pipeline Job. Is fixing the above error and allowing groovy interfaces within the Jenkins Pipeline something that is on the current schedule? Is so, what is the timeline for this being added??
            jglick Jesse Glick made changes -
            Epic Link JENKINS-35390 [ 171183 ]
            fchuong Frédéric Chuong added a comment - Upstream issue: https://github.com/cloudbees/groovy-cps/issues/26
            fchuong Frédéric Chuong made changes -
            Remote Link This issue links to "groovy-cps: Interfaces cannot be declared (Web Link)" [ 14534 ]
            rtyler R. Tyler Croy made changes -
            Workflow JNJira [ 162525 ] JNJira + In-Review [ 180958 ]
            duemir Denys Digtiar added a comment -

            Seems to be resolved in workflow-cps:2.12 which depends on the groovy-cps:1.9 which includes the fix mentioned in the previous comment.

            duemir Denys Digtiar added a comment - Seems to be resolved in workflow-cps:2.12 which depends on the groovy-cps:1.9 which includes the fix mentioned in the previous comment.
            jglick Jesse Glick added a comment -

            duemir did you try the originally reported idiom, using the as keyword? I suspect that is the issue, not merely using interface.

            jglick Jesse Glick added a comment - duemir did you try the originally reported idiom, using the as keyword? I suspect that is the issue, not merely using interface .
            abayer Andrew Bayer made changes -
            Component/s pipeline-general [ 21692 ]
            abayer Andrew Bayer made changes -
            Component/s workflow-plugin [ 18820 ]
            jglick Jesse Glick made changes -
            Component/s workflow-cps-plugin [ 21713 ]
            Component/s pipeline [ 21692 ]
            jglick Jesse Glick made changes -
            Link This issue is duplicated by JENKINS-36038 [ JENKINS-36038 ]
            duemir Denys Digtiar added a comment -

            jglick My bad. Didn't think about it.
            Tried as keyword, it is still causing an error.

            duemir Denys Digtiar added a comment - jglick My bad. Didn't think about it. Tried as keyword, it is still causing an error.
            abayer Andrew Bayer made changes -
            Labels groovy complex-cps-code groovy triaged-2018-11

            People

              kohsuke Kohsuke Kawaguchi
              tomjdalton Thomas Dalton
              Votes:
              4 Vote for this issue
              Watchers:
              10 Start watching this issue

              Dates

                Created:
                Updated: