Skip to content

Commit dec5de3

Browse files
somiljain2006timja
andauthored
Add API endpoint to apply new JCasC configuration with yaml in body (#2815)
Co-authored-by: Tim Jacomb <[email protected]> Co-authored-by: Tim Jacomb <[email protected]>
1 parent fc1796c commit dec5de3

3 files changed

Lines changed: 217 additions & 0 deletions

File tree

docs/features/configurationReload.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ $ curl -X POST -G -d @/path/to/secret/file "JENKINS_URL/reload-configuration-as-
2424
permissions. Since Jenkins 2.96 CRUMB is not needed for API tokens.
2525
- via [Jenkins CLI](https://www.jenkins.io/doc/book/managing/cli/): with the Jenkins CLI (either with SSH or JAR), the command `java -jar jenkins-cli.jar -s ${JENKINS_URL} reload-jcasc-configuration` triggers a configuration reload.
2626
This Jenkins CLI command is only present when the plugin `configuration-as-code` is installed, and reported in the help message:
27+
28+
- via http POST to `JENKINS_URL/configuration-as-code/configure`
29+
This endpoint allows you to send your configuration as code directly in the HTTP POST body.
30+
31+
To use this endpoint, you must:
32+
- Authenticate the request using the username and API token of a user with `Administer` permissions.
33+
34+
**Example Usage:**
35+
```sh
36+
$ curl -X POST -u admin:YOUR_API_TOKEN \
37+
--data-binary @jenkins.yaml \
38+
"JENKINS_URL/configuration-as-code/configure"
2739

2840
```shell
2941
$ java -jar jenkins-cli.jar -s ${JENKINS_URL} help

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,4 +985,47 @@ public static String printThrowable(@NonNull Throwable t) {
985985
.replaceAll("\t", " ");
986986
return s.substring(0, s.lastIndexOf(")") + 1);
987987
}
988+
989+
@RequirePOST
990+
@Restricted(NoExternalUse.class)
991+
@SuppressWarnings("unused")
992+
public void doConfigure(StaplerRequest2 req, StaplerResponse2 res) throws Exception {
993+
994+
if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
995+
res.sendError(HttpServletResponse.SC_FORBIDDEN, "Requires ADMINISTER permission");
996+
return;
997+
}
998+
999+
if (req.getContentLength() <= 0) {
1000+
res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
1001+
res.setContentType("application/json; charset=utf-8");
1002+
JSONArray errors = new JSONArray();
1003+
JSONObject error = new JSONObject();
1004+
error.put("line", -1);
1005+
error.put("message", "Request body cannot be empty.");
1006+
errors.add(error);
1007+
res.getWriter().print(errors);
1008+
return;
1009+
}
1010+
1011+
try {
1012+
configureWith(YamlSource.of(req));
1013+
res.setStatus(HttpServletResponse.SC_OK);
1014+
res.setContentType("text/plain; charset=utf-8");
1015+
res.getWriter().print("Configuration successfully applied.");
1016+
1017+
} catch (Exception e) {
1018+
LOGGER.log(Level.SEVERE, "Failed to apply configuration from POST payload", e);
1019+
res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
1020+
res.setContentType("application/json; charset=utf-8");
1021+
1022+
JSONArray errors = new JSONArray();
1023+
JSONObject error = new JSONObject();
1024+
error.put("line", -1);
1025+
error.put("message", e.getMessage());
1026+
errors.add(error);
1027+
1028+
res.getWriter().print(errors);
1029+
}
1030+
}
9881031
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package io.jenkins.plugins.casc;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.containsString;
5+
import static org.hamcrest.Matchers.is;
6+
7+
import java.net.URL;
8+
import jenkins.model.Jenkins;
9+
import org.htmlunit.HttpMethod;
10+
import org.htmlunit.WebRequest;
11+
import org.htmlunit.WebResponse;
12+
import org.junit.Rule;
13+
import org.junit.Test;
14+
import org.jvnet.hudson.test.JenkinsRule;
15+
import org.jvnet.hudson.test.MockAuthorizationStrategy;
16+
17+
public class ConfigurationAsCodeApiTest {
18+
19+
@Rule
20+
public JenkinsRule j = new JenkinsRule();
21+
22+
private static final String ENDPOINT = "configuration-as-code/configure";
23+
private static final String YAML_CONTENT_TYPE = "application/yaml";
24+
private static final String ADMIN = "admin";
25+
26+
private WebRequest webRequest(HttpMethod method) throws Exception {
27+
return new WebRequest(new URL(j.getURL(), ENDPOINT), method);
28+
}
29+
30+
private WebRequest yamlPost(String requestBody) throws Exception {
31+
WebRequest request = webRequest(HttpMethod.POST);
32+
request.setAdditionalHeader("Content-Type", YAML_CONTENT_TYPE);
33+
if (requestBody != null) {
34+
request.setRequestBody(requestBody);
35+
}
36+
return request;
37+
}
38+
39+
private void configureAdminSecurity() {
40+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
41+
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
42+
.grant(Jenkins.ADMINISTER)
43+
.everywhere()
44+
.to(ADMIN));
45+
}
46+
47+
@Test
48+
public void testDoConfigure_RequiresPost() throws Exception {
49+
try (JenkinsRule.WebClient wc = j.createWebClient()) {
50+
wc.setThrowExceptionOnFailingStatusCode(false);
51+
52+
WebResponse response = wc.getPage(webRequest(HttpMethod.GET)).getWebResponse();
53+
assertThat(response.getStatusCode(), is(405));
54+
}
55+
}
56+
57+
@Test
58+
public void testDoConfigure_Success() throws Exception {
59+
configureAdminSecurity();
60+
61+
try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) {
62+
wc.setThrowExceptionOnFailingStatusCode(false);
63+
64+
WebResponse response = wc.getPage(yamlPost("jenkins:\n systemMessage: 'Webhook Success'"))
65+
.getWebResponse();
66+
67+
assertThat(response.getStatusCode(), is(200));
68+
assertThat(j.jenkins.getSystemMessage(), is("Webhook Success"));
69+
}
70+
}
71+
72+
@Test
73+
public void testDoConfigure_InvalidYaml() throws Exception {
74+
configureAdminSecurity();
75+
76+
try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) {
77+
wc.setThrowExceptionOnFailingStatusCode(false);
78+
79+
WebResponse response =
80+
wc.getPage(yamlPost("jenkins:\n systemMessage: [invalid")).getWebResponse();
81+
82+
assertThat(response.getStatusCode(), is(400));
83+
assertThat(response.getContentAsString(), containsString("message"));
84+
}
85+
}
86+
87+
@Test
88+
public void testDoConfigure_NonAdminForbidden() throws Exception {
89+
try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken("user")) {
90+
wc.setThrowExceptionOnFailingStatusCode(false);
91+
92+
WebRequest request = yamlPost("jenkins:\n systemMessage: 'fail'");
93+
94+
WebResponse response = wc.getPage(request).getWebResponse();
95+
assertThat(response.getStatusCode(), is(403));
96+
}
97+
}
98+
99+
@Test
100+
public void testDoConfigure_Unauthenticated() throws Exception {
101+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
102+
j.jenkins.setAuthorizationStrategy(
103+
new MockAuthorizationStrategy().grant(Jenkins.READ).everywhere().toEveryone());
104+
105+
try (JenkinsRule.WebClient wc = j.createWebClient()) {
106+
wc.setThrowExceptionOnFailingStatusCode(false);
107+
108+
WebRequest request = yamlPost("jenkins:\n systemMessage: 'anonymous bypass attempt'");
109+
110+
WebResponse response = wc.getPage(request).getWebResponse();
111+
assertThat(response.getStatusCode(), is(403));
112+
}
113+
}
114+
115+
@Test
116+
public void testDoConfigure_EmptyBody() throws Exception {
117+
configureAdminSecurity();
118+
119+
try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) {
120+
wc.setThrowExceptionOnFailingStatusCode(false);
121+
122+
WebResponse response = wc.getPage(yamlPost(null)).getWebResponse();
123+
124+
assertThat(response.getStatusCode(), is(400));
125+
assertThat(response.getContentAsString(), containsString("message"));
126+
}
127+
}
128+
129+
@Test
130+
public void testDoConfigure_MalformedStructure() throws Exception {
131+
configureAdminSecurity();
132+
133+
try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) {
134+
wc.setThrowExceptionOnFailingStatusCode(false);
135+
136+
WebResponse response = wc.getPage(yamlPost("jenkins:\n invalidRoot:\n foo: bar"))
137+
.getWebResponse();
138+
139+
assertThat(response.getStatusCode(), is(400));
140+
assertThat(response.getContentAsString(), containsString("message"));
141+
}
142+
}
143+
144+
@Test
145+
public void testDoConfigure_ValidYaml_NoChanges() throws Exception {
146+
configureAdminSecurity();
147+
148+
try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) {
149+
wc.setThrowExceptionOnFailingStatusCode(false);
150+
151+
WebResponse response1 = wc.getPage(yamlPost("jenkins:\n systemMessage: 'Idempotency Test'"))
152+
.getWebResponse();
153+
assertThat(response1.getStatusCode(), is(200));
154+
assertThat(j.jenkins.getSystemMessage(), is("Idempotency Test"));
155+
156+
WebResponse response2 = wc.getPage(yamlPost("jenkins:\n systemMessage: 'Idempotency Test'"))
157+
.getWebResponse();
158+
assertThat(response2.getStatusCode(), is(200));
159+
assertThat(j.jenkins.getSystemMessage(), is("Idempotency Test"));
160+
}
161+
}
162+
}

0 commit comments

Comments
 (0)