Skip to content

Commit da0008a

Browse files
committed
Ensure ConfigurationContext respects global strictExport setting
1 parent 7a832a2 commit da0008a

7 files changed

Lines changed: 114 additions & 17 deletions

File tree

plugin/src/main/java/io/jenkins/plugins/casc/Attribute.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ public CNode describe(Owner instance, ConfigurationContext context) throws Confi
273273
return seq;
274274
}
275275
return _describe(c, context, o, shouldBeMasked);
276-
} catch (Exception | AssertionError e) {
276+
} catch (Exception | /* Jenkins.getDescriptorOrDie */ AssertionError e) {
277277
if (context.isStrictExport()) {
278278
if (e instanceof ConfiguratorException) {
279279
throw (ConfiguratorException) e;

plugin/src/main/java/io/jenkins/plugins/casc/CasCGlobalConfig.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
public class CasCGlobalConfig extends GlobalConfiguration {
1616

1717
private String configurationPath;
18+
private boolean strictExport = false;
1819

1920
@DataBoundConstructor
2021
public CasCGlobalConfig(String configurationPath) {
@@ -40,6 +41,15 @@ public void setConfigurationPath(String configurationPath) {
4041
this.configurationPath = configurationPath;
4142
}
4243

44+
public boolean isStrictExport() {
45+
return strictExport;
46+
}
47+
48+
@DataBoundSetter
49+
public void setStrictExport(boolean strictExport) {
50+
this.strictExport = strictExport;
51+
}
52+
4353
@Override
4454
public boolean configure(StaplerRequest2 req, JSONObject json) throws FormException {
4555
req.bindJSON(this, json);

plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -595,17 +595,11 @@ public void doReference(StaplerRequest2 req, StaplerResponse2 res) throws Except
595595

596596
@Restricted(NoExternalUse.class)
597597
public void export(OutputStream out) throws Exception {
598-
export(out, false);
599-
}
600-
601-
@Restricted(NoExternalUse.class)
602-
public void export(OutputStream out, boolean strict) throws Exception {
603598
final List<NodeTuple> tuples = new ArrayList<>();
604599

605600
final ConfigurationContext context = new ConfigurationContext(registry);
606-
context.setStrictExport(strict);
607-
for (RootElementConfigurator root : RootElementConfigurator.all()) {
608-
final CNode config = root.describe(root.getTargetComponent(context), context);
601+
for (RootElementConfigurator<?> root : RootElementConfigurator.all()) {
602+
final CNode config = describeRoot(root, context);
609603
final Node valueNode = toYaml(config);
610604
if (valueNode == null) {
611605
continue;
@@ -621,6 +615,11 @@ public void export(OutputStream out, boolean strict) throws Exception {
621615
}
622616
}
623617

618+
private <T> CNode describeRoot(RootElementConfigurator<T> root, ConfigurationContext context) throws Exception {
619+
T component = root.getTargetComponent(context);
620+
return root.describe(component, context);
621+
}
622+
624623
@Restricted(NoExternalUse.class) // for testing only
625624
public static void serializeYamlNode(Node root, Writer writer) throws IOException {
626625
DumperOptions options = new DumperOptions();

plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationContext.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import java.lang.reflect.Type;
88
import java.util.ArrayList;
99
import java.util.List;
10+
import jenkins.model.GlobalConfiguration;
11+
import jenkins.model.Jenkins;
1012
import org.apache.commons.lang3.math.NumberUtils;
1113
import org.kohsuke.stapler.Stapler;
1214

@@ -60,6 +62,13 @@ public ConfigurationContext(ConfiguratorRegistry registry, String mergeStrategy)
6062
this.mergeStrategy = mergeStrategy != null
6163
? mergeStrategy
6264
: getPropertyOrEnv(CASC_MERGE_STRATEGY_ENV, CASC_MERGE_STRATEGY_PROPERTY);
65+
Jenkins jenkins = Jenkins.getInstanceOrNull();
66+
if (jenkins != null) {
67+
CasCGlobalConfig config = GlobalConfiguration.all().get(CasCGlobalConfig.class);
68+
if (config != null) {
69+
this.strictExport = config.isStrictExport();
70+
}
71+
}
6372
}
6473

6574
private String getPropertyOrEnv(String envKey, String proKey) {

plugin/src/test/java/io/jenkins/plugins/casc/AttributeTest.java

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package io.jenkins.plugins.casc;
22

3+
import static java.lang.reflect.Proxy.newProxyInstance;
4+
import static org.hamcrest.MatcherAssert.assertThat;
5+
import static org.hamcrest.Matchers.containsString;
36
import static org.junit.jupiter.api.Assertions.assertFalse;
47
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
58
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -267,9 +270,10 @@ public <T> Configurator<T> lookup(Type type) {
267270
CNode node = attr.describe(dummyInstance, context);
268271

269272
assertInstanceOf(Scalar.class, node, "Should return a Scalar node on failure in non-strict mode");
270-
assertTrue(
271-
((Scalar) node).getValue().contains("FAILED TO EXPORT"),
272-
"Scalar should contain the fallback failure message");
273+
assertThat(
274+
"Scalar should contain the fallback failure message",
275+
((Scalar) node).getValue(),
276+
containsString("FAILED TO EXPORT"));
273277

274278
context.setStrictExport(true);
275279
ConfiguratorException exception = assertThrows(
@@ -279,8 +283,62 @@ public <T> Configurator<T> lookup(Type type) {
279283
},
280284
"Should completely abort and throw ConfiguratorException in strict mode");
281285

282-
assertTrue(
283-
exception.getMessage().contains("No configurator found"),
284-
"Exception message should accurately reflect the missing configurator");
286+
assertThat(exception.getMessage(), containsString("No configurator found"));
287+
}
288+
289+
@Test
290+
@SuppressWarnings({"ExtractMethodRecommender", "unchecked"})
291+
void describeWrapsGenericExceptionsInStrictMode() throws Exception {
292+
293+
Configurator<?> dummyConfigurator = (Configurator<?>) newProxyInstance(
294+
Configurator.class.getClassLoader(), new Class<?>[] {Configurator.class}, (proxy, method, args) -> {
295+
String methodName = method.getName();
296+
return switch (methodName) {
297+
case "equals" -> args != null && args.length == 1 && proxy == args[0];
298+
case "hashCode" -> System.identityHashCode(proxy);
299+
case "toString" -> "DummyConfiguratorProxy";
300+
case "describe" ->
301+
throw new IllegalStateException("Intentional generic failure from dummy configurator");
302+
default -> null;
303+
};
304+
});
305+
306+
ConfiguratorRegistry dummyRegistry = new ConfiguratorRegistry() {
307+
@Override
308+
public RootElementConfigurator<?> lookupRootElement(String name) {
309+
return null;
310+
}
311+
312+
@Override
313+
@NonNull
314+
public <T> Configurator<T> lookupOrFail(Type type) {
315+
return (Configurator<T>) dummyConfigurator;
316+
}
317+
318+
@Override
319+
public <T> Configurator<T> lookup(Type type) {
320+
return (Configurator<T>) dummyConfigurator;
321+
}
322+
};
323+
324+
ConfigurationContext context = new ConfigurationContext(dummyRegistry);
325+
context.setStrictExport(true);
326+
327+
Attribute<NonSecretField, String> attr = new Attribute<>("passwordPath", String.class);
328+
NonSecretField dummyInstance = new NonSecretField("my-dummy-path");
329+
330+
ConfiguratorException exception = assertThrows(
331+
ConfiguratorException.class,
332+
() -> {
333+
attr.describe(dummyInstance, context);
334+
},
335+
"Should wrap generic exception into a ConfiguratorException in strict mode");
336+
337+
assertThat(
338+
exception.getMessage(),
339+
containsString("Failed to export io.jenkins.plugins.casc.AttributeTest$NonSecretField#passwordPath"));
340+
assertThat(
341+
exception.getCause().getMessage(),
342+
containsString("Intentional generic failure from dummy configurator"));
285343
}
286344
}

plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.nio.file.Files;
1212
import java.nio.file.Path;
1313
import java.util.List;
14+
import jenkins.model.GlobalConfiguration;
1415
import jenkins.model.Jenkins;
1516
import org.htmlunit.HttpMethod;
1617
import org.htmlunit.WebRequest;
@@ -193,9 +194,14 @@ public void testDoReplace_ValidSource() throws Exception {
193194
public void testExportStrictMode_SuccessOnCleanJenkins() throws Exception {
194195
configureAdminSecurity();
195196

197+
CasCGlobalConfig config = GlobalConfiguration.all().get(CasCGlobalConfig.class);
198+
if (config != null) {
199+
config.setStrictExport(true);
200+
}
201+
196202
ByteArrayOutputStream out = new ByteArrayOutputStream();
197203

198-
ConfigurationAsCode.get().export(out, true);
204+
ConfigurationAsCode.get().export(out);
199205

200206
String exportedYaml = out.toString(StandardCharsets.UTF_8);
201207

test-harness/src/main/java/io/jenkins/plugins/casc/misc/JenkinsConfiguredRule.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package io.jenkins.plugins.casc.misc;
22

3+
import io.jenkins.plugins.casc.CasCGlobalConfig;
34
import io.jenkins.plugins.casc.ConfigurationAsCode;
45
import java.io.ByteArrayOutputStream;
56
import java.nio.charset.StandardCharsets;
7+
import jenkins.model.GlobalConfiguration;
68
import org.jvnet.hudson.test.JenkinsRule;
79

810
public class JenkinsConfiguredRule extends JenkinsRule {
@@ -17,7 +19,20 @@ public class JenkinsConfiguredRule extends JenkinsRule {
1719
public String exportToString(boolean strict) throws Exception {
1820
final ByteArrayOutputStream out = new ByteArrayOutputStream();
1921

20-
ConfigurationAsCode.get().export(out, strict);
22+
CasCGlobalConfig config = GlobalConfiguration.all().get(CasCGlobalConfig.class);
23+
boolean originalStrict = config != null && config.isStrictExport();
24+
25+
if (config != null) {
26+
config.setStrictExport(strict);
27+
}
28+
29+
try {
30+
ConfigurationAsCode.get().export(out);
31+
} finally {
32+
if (config != null) {
33+
config.setStrictExport(originalStrict);
34+
}
35+
}
2136

2237
return out.toString(StandardCharsets.UTF_8);
2338
}

0 commit comments

Comments
 (0)