-
Improvement
-
Resolution: Fixed
-
Minor
-
-
1.30
Configuration-as-Code is the (proposed) mechanism to configure jenkins master automatically from plain text recipes. It relies on component internal model auto-discovery, and as such assume plugins do follow best practices for UI data-binding and internal design.
Using the export feature to generate a configuration yaml-file
it's partially supported, but significant configuration attributes don't follow expected design.
as a resume
- manual JSON parsing vs data-binding
- no setters for databound configuration elements
- optional block vs optional property
Part of the experienced Exception:
"FAILED TO EXPORT hudson.plugins.gradle.GradleInstallation$DescriptorImpl\ \ : \nio.jenkins.plugins.casc.ConfiguratorException: Can't read attribute 'installations'\ \ from hudson.plugins.gradle.GradleInstallation$DescriptorImpl@2dfa0e31\n\tat\ \ io.jenkins.plugins.casc.Attribute._getValue(Attribute.java:252)\n\tat io.jenkins.plugins.casc.Attribute.getValue(Attribute.java:166)\n\ \tat io.jenkins.plugins.casc.Attribute.equals(Attribute.java:199)\n\tat io.jenkins.plugins.casc.BaseConfigurator.compare(BaseConfigurator.java:367)\n\ \tat io.jenkins.plugins.casc.impl.configurators.DescriptorConfigurator.describe(DescriptorConfigurator.java:65)\n\ \tat io.jenkins.plugins.casc.impl.configurators.GlobalConfigurationCategoryConfigurator.describe(GlobalConfigurationCategoryConfigurator.java:94)\n\ \tat io.jenkins.plugins.casc.impl.configurators.GlobalConfigurationCategoryConfigurator.lambda$describe$6(GlobalConfigurationCategoryConfigurator.java:87)\n\ \tat java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)\n\ \tat java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)\n\ \tat java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)\n\ \tat java.util.Iterator.forEachRemaining(Iterator.java:116)\n\tat java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)\n\ \tat java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)\n\t\ at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)\n\ \tat java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)\n\ \tat java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)\n\ \tat java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)\n\t\ at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)\n\t\ at io.jenkins.plugins.casc.impl.configurators.GlobalConfigurationCategoryConfigurator.describe(GlobalConfigurationCategoryConfigurator.java:87)\n\ \tat io.jenkins.plugins.casc.impl.configurators.GlobalConfigurationCategoryConfigurator.describe(GlobalConfigurationCategoryConfigurator.java:30)\n\ \tat io.jenkins.plugins.casc.ConfigurationAsCode.export(ConfigurationAsCode.java:406)\n\ \tat io.jenkins.plugins.casc.ConfigurationAsCode.doExport(ConfigurationAsCode.java:396)\n\ \tat java.lang.invoke.MethodHandle.invokeWithArguments(MethodHandle.java:627)\n\ \tat org.kohsuke.stapler.Function$MethodFunction.invoke(Function.java:343)\n\t\ at org.kohsuke.stapler.interceptor.RequirePOST$Processor.invoke(RequirePOST.java:77)\n\ \tat org.kohsuke.stapler.PreInvokeInterceptedFunction.invoke(PreInvokeInterceptedFunction.java:26)\n\ \tat org.kohsuke.stapler.Function.bindAndInvoke(Function.java:184)\n\tat org.kohsuke.stapler.Function.bindAndInvokeAndServeResponse(Function.java:117)\n\ \tat org.kohsuke.stapler.MetaClass$1.doDispatch(MetaClass.java:129)\n\tat org.kohsuke.stapler.NameBasedDispatcher.dispatch(NameBasedDispatcher.java:58)\n\ \tat org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:734)\n\tat org.kohsuke.stapler.Stapler.invoke(Stapler.java:864)\n\ \tat org.kohsuke.stapler.MetaClass$10.dispatch(MetaClass.java:374)\n\tat org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:734)\n\ \tat org.kohsuke.stapler.Stapler.invoke(Stapler.java:864)\n\tat org.kohsuke.stapler.Stapler.invoke(Stapler.java:668)\n\ \tat org.kohsuke.stapler.Stapler.service(Stapler.java:238)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:790)\n\ \tat org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:865)\n\t\ at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1655)\n\ \tat hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:154)\n\ \tat org.jenkinsci.plugins.ssegateway.Endpoint$SSEListenChannelFilter.doFilter(Endpoint.java:225)\n\ \tat hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:151)\n\ \tat io.jenkins.blueocean.ResourceCacheControl.doFilter(ResourceCacheControl.java:134)\n\ \tat hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:151)\n\ \tat io.jenkins.blueocean.auth.jwt.impl.JwtAuthenticationFilter.doFilter(JwtAuthenticationFilter.java:61)\n\ \tat hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:151)\n\ \tat com.smartcodeltd.jenkinsci.plugin.assetbundler.filters.LessCSS.doFilter(LessCSS.java:47)\n\ \tat hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:151)\n\ \tat hudson.util.PluginServletFilter.doFilter(PluginServletFilter.java:157)\n\t\ at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1642)\n\ \tat hudson.security.csrf.CrumbFilter.doFilter(CrumbFilter.java:99)\n\tat org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1642)\n\ \tat hudson.security.ChainedServletFilter$1.doFilter(ChainedServletFilter.java:84)\n\ \tat hudson.security.UnwrapSecurityExceptionFilter.doFilter(UnwrapSecurityExceptionFilter.java:51)\n\ \tat hudson.security.ChainedServletFilter$1.doFilter(ChainedServletFilter.java:87)\n\ \tat jenkins.security.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:117)\n\ \tat hudson.security.ChainedServletFilter$1.doFilter(ChainedServletFilter.java:87)\n\ \tat org.acegisecurity.providers.anonymous.AnonymousProcessingFilter.doFilter(AnonymousProcessingFilter.java:125)\n\ \tat hudson.security.ChainedServletFilter$1.doFilter(ChainedServletFilter.java:87)\n\ \tat org.acegisecurity.ui.rememberme.RememberMeProcessingFilter.doFilter(RememberMeProcessingFilter.java:142)\n\ \tat hudson.security.ChainedServletFilter$1.doFilter(ChainedServletFilter.java:87)\n\ \tat org.acegisecurity.ui.AbstractProcessingFilter.doFilter(AbstractProcessingFilter.java:271)\n\ \tat hudson.security.ChainedServletFilter$1.doFilter(ChainedServletFilter.java:87)\n\ \tat jenkins.security.BasicHeaderProcessor.doFilter(BasicHeaderProcessor.java:93)\n\ \tat hudson.security.ChainedServletFilter$1.doFilter(ChainedServletFilter.java:87)\n\ \tat org.acegisecurity.context.HttpSessionContextIntegrationFilter.doFilter(HttpSessionContextIntegrationFilter.java:249)\n\ \tat hudson.security.HttpSessionContextIntegrationFilter2.doFilter(HttpSessionContextIntegrationFilter2.java:67)\n\ \tat hudson.security.ChainedServletFilter$1.doFilter(ChainedServletFilter.java:87)\n\ \tat hudson.security.ChainedServletFilter.doFilter(ChainedServletFilter.java:90)\n\ \tat hudson.security.HudsonFilter.doFilter(HudsonFilter.java:171)\n\tat org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1642)\n\ \tat org.kohsuke.stapler.compression.CompressionFilter.doFilter(CompressionFilter.java:49)\n\ \tat org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1642)\n\ \tat hudson.util.CharacterEncodingFilter.doFilter(CharacterEncodingFilter.java:82)\n\ \tat org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1642)\n\ \tat org.kohsuke.stapler.DiagnosticThreadNameFilter.doFilter(DiagnosticThreadNameFilter.java:30)\n\ \tat org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1642)\n\ \tat org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:533)\n\ \tat org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:146)\n\ \tat org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:524)\n\ \tat org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)\n\ \tat org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:257)\n\ \tat org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1595)\n\ \tat org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255)\n\ \tat org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1317)\n\ \tat org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)\n\ \tat org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:473)\n\ \tat org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1564)\n\ \tat org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)\n\ \tat org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1219)\n\ \tat org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)\n\ \tat org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)\n\ \tat org.eclipse.jetty.server.Server.handle(Server.java:531)\n\tat org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:352)\n\ \tat org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:260)\n\ \tat org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:281)\n\ \tat org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:102)\n\tat org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:118)\n\ \tat org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:333)\n\ \tat org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:310)\n\ \tat org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)\n\ \tat org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)\n\ \tat org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)\n\ \tat winstone.BoundedExecutorService.lambda$scheduleNext$0(BoundedExecutorService.java:80)\n\ \tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\n\ \tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\n\ \tat java.lang.Thread.run(Thread.java:748)\nCaused by: java.lang.reflect.InvocationTargetException\n\ \tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\ \tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\ \tat java.lang.reflect.Method.invoke(Method.java:498)\n\tat io.jenkins.plugins.casc.Attribute._getValue(Attribute.java:234)\n\ \t... 107 more\nCaused by: java.lang.NullPointerException\n\tat hudson.plugins.gradle.GradleInstallation$DescriptorImpl.getInstallations(GradleInstallation.java:120)\n\ \tat hudson.plugins.gradle.GradleInstallation$DescriptorImpl.getInstallations(GradleInstallation.java:97)\n\ \t... 112 more\n"
- links to
The issue is due to JCasC export feature design : as we only want to export things that differ from default values, we create a fresh new `GradleInstallation$DescriptorImpl` instance and compare properties.
But gradle plugin did manage the transition from `Gradle.DescriptorImpl` to `ToolDescriptor` relying on an `@Inject` delegate, which I think is a nice, concise and clean way to handle this. When JCasC do create a new instance it doesn't inject dependencies and the delegate call results in a NPE.
I'm not sure the best way to handle this. I like the @Inject-based delegate approach, compared to other implementations which do a systematic lookup to legacy Descriptor. On the other hand the root issue maybe is that those approach don't migrate data, they just pretend they implement the new API but tried to avoid having to manage data migration. Maybe a correct way to handle this is for `Gradle.DescriptorImpl` (and other) to populate `GradleInstallation$DescriptorImpl#installations` when some legacy data get loaded, and fully follow the intended `ToolDescriptor` design to host `installations` property.