Skip to content
Merged
12 changes: 12 additions & 0 deletions docs/features/configurationReload.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ $ curl -X POST -G -d @/path/to/secret/file "JENKINS_URL/reload-configuration-as-
permissions. Since Jenkins 2.96 CRUMB is not needed for API tokens.
- 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.
This Jenkins CLI command is only present when the plugin `configuration-as-code` is installed, and reported in the help message:

- via http POST to `JENKINS_URL/configuration-as-code/configure`
This endpoint allows you to send your configuration as code directly in the HTTP POST body.

To use this endpoint, you must:
- Authenticate the request using the username and API token of a user with `Administer` permissions.

**Example Usage:**
```sh
$ curl -X POST -u admin:YOUR_API_TOKEN \
--data-binary @jenkins.yaml \
"JENKINS_URL/configuration-as-code/configure"

```shell
$ java -jar jenkins-cli.jar -s ${JENKINS_URL} help
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -985,4 +985,47 @@ public static String printThrowable(@NonNull Throwable t) {
.replaceAll("\t", " ");
return s.substring(0, s.lastIndexOf(")") + 1);
}

@RequirePOST
@Restricted(NoExternalUse.class)
@SuppressWarnings("unused")
public void doConfigure(StaplerRequest2 req, StaplerResponse2 res) throws Exception {

if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
res.sendError(HttpServletResponse.SC_FORBIDDEN, "Requires ADMINISTER permission");
return;
}

if (req.getContentLength() <= 0) {
res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
res.setContentType("application/json; charset=utf-8");
JSONArray errors = new JSONArray();
JSONObject error = new JSONObject();
error.put("line", -1);
error.put("message", "Request body cannot be empty.");
errors.add(error);
res.getWriter().print(errors);
return;
}

try {
configureWith(YamlSource.of(req));
res.setStatus(HttpServletResponse.SC_OK);
res.setContentType("text/plain; charset=utf-8");
res.getWriter().print("Configuration successfully applied.");

} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Failed to apply configuration from POST payload", e);
res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
res.setContentType("application/json; charset=utf-8");

JSONArray errors = new JSONArray();
JSONObject error = new JSONObject();
error.put("line", -1);
error.put("message", e.getMessage());
errors.add(error);

res.getWriter().print(errors);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
package io.jenkins.plugins.casc;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import jakarta.servlet.ServletRequest;
import java.net.URL;
import jenkins.model.Jenkins;
import org.htmlunit.HttpMethod;
import org.htmlunit.WebRequest;
import org.htmlunit.WebResponse;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient;
import org.jvnet.hudson.test.MockAuthorizationStrategy;

public class ConfigurationAsCodeApiTest {

@Rule
public JenkinsRule j = new JenkinsRule();

@Test
public void testDoConfigure_RequiresPost() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());

j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(jenkins.model.Jenkins.ADMINISTER)
.everywhere()
.to("admin"));

WebClient wc = j.createWebClient();
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
wc.login("admin", "admin");
wc.setThrowExceptionOnFailingStatusCode(false);

WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.GET);
WebResponse response = wc.getPage(request).getWebResponse();

assertTrue(response.getStatusCode() == 404 || response.getStatusCode() == 405);
}

@Test
public void testDoConfigure_Success() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());

j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(jenkins.model.Jenkins.ADMINISTER)
.everywhere()
.to("admin"));

WebClient wc = j.createWebClient();
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
wc.login("admin", "admin");
wc.setThrowExceptionOnFailingStatusCode(false);

WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST);
request.setAdditionalHeader("Content-Type", "application/yaml");
request.setRequestBody("jenkins:\n systemMessage: 'Webhook Success'");

var crumbIssuer = j.jenkins.getCrumbIssuer();

if (crumbIssuer != null) {
request.setAdditionalHeader(
crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null));
}

WebResponse response = wc.getPage(request).getWebResponse();

assertEquals(200, response.getStatusCode());
assertEquals("Webhook Success", j.jenkins.getSystemMessage());
}

@Test
public void testDoConfigure_InvalidYaml() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());

j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(jenkins.model.Jenkins.ADMINISTER)
.everywhere()
.to("admin"));

WebClient wc = j.createWebClient();
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
wc.login("admin", "admin");
wc.setThrowExceptionOnFailingStatusCode(false);

WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST);

request.setRequestBody("jenkins:\n systemMessage: [invalid");

var crumbIssuer = j.jenkins.getCrumbIssuer();

if (crumbIssuer != null) {
request.setAdditionalHeader(
crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null));
}

WebResponse response = wc.getPage(request).getWebResponse();

assertEquals(400, response.getStatusCode());
assertTrue(response.getContentAsString().contains("message"));
}

@Test
public void testDoConfigure_NonAdminForbidden() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(Jenkins.READ)
.everywhere()
.to("user")
.grant(jenkins.model.Jenkins.ADMINISTER)
.everywhere()
.to("admin"));
Comment thread
somiljain2006 marked this conversation as resolved.
Outdated

WebClient wc = j.createWebClient();
wc.login("user", "user");
wc.setThrowExceptionOnFailingStatusCode(false);

WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST);
request.setRequestBody("jenkins:\n systemMessage: 'fail'");

var crumbIssuer = j.jenkins.getCrumbIssuer();

if (crumbIssuer != null) {
request.setAdditionalHeader(
crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null));
}
Comment thread
somiljain2006 marked this conversation as resolved.
Outdated

WebResponse response = wc.getPage(request).getWebResponse();

assertEquals(403, response.getStatusCode());
}

@Test
public void testDoConfigure_Unauthenticated() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new org.jvnet.hudson.test.MockAuthorizationStrategy()
.grant(Jenkins.READ)
.everywhere()
.toEveryone());

WebClient wc = j.createWebClient();
wc.setThrowExceptionOnFailingStatusCode(false);

WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST);

request.setRequestBody("jenkins:\n systemMessage: 'anonymous bypass attempt'");

WebResponse response = wc.getPage(request).getWebResponse();

assertEquals(403, response.getStatusCode());
}

@Test
public void testDoConfigure_MissingCrumb() throws Exception {
Comment thread
somiljain2006 marked this conversation as resolved.
Outdated
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(Jenkins.ADMINISTER)
.everywhere()
.to("admin"));

WebClient wc = j.createWebClient();
wc.login("admin", "admin");
wc.setThrowExceptionOnFailingStatusCode(false);

WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST);

request.setRequestBody("jenkins:\n systemMessage: 'no crumb'");

WebResponse response = wc.getPage(request).getWebResponse();

assertEquals(403, response.getStatusCode());
assertTrue(response.getContentAsString().contains("No valid crumb"));
}

@Test
public void testDoConfigure_EmptyBody() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(Jenkins.ADMINISTER)
.everywhere()
.to("admin"));

WebClient wc = j.createWebClient();
wc.login("admin", "admin");

wc.setThrowExceptionOnFailingStatusCode(false);

WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST);
request.setAdditionalHeader("Content-Type", "application/yaml");

var crumbIssuer = j.jenkins.getCrumbIssuer();

if (crumbIssuer != null) {
request.setAdditionalHeader(
crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null));
}

WebResponse response = wc.getPage(request).getWebResponse();

assertEquals(400, response.getStatusCode());
assertTrue(response.getContentAsString().contains("message"));
}

@Test
public void testDoConfigure_MalformedStructure() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());

j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(jenkins.model.Jenkins.ADMINISTER)
.everywhere()
.to("admin"));

WebClient wc = j.createWebClient();
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
wc.login("admin", "admin");
wc.setThrowExceptionOnFailingStatusCode(false);

WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST);
request.setAdditionalHeader("Content-Type", "application/yaml");
request.setRequestBody("jenkins:\n invalidRoot:\n foo: bar");

var crumbIssuer = j.jenkins.getCrumbIssuer();

if (crumbIssuer != null) {
request.setAdditionalHeader(
crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null));
}

WebResponse response = wc.getPage(request).getWebResponse();

assertEquals(400, response.getStatusCode());
assertTrue(response.getContentAsString().contains("message"));
}

@Test
public void testDoConfigure_ValidYaml_NoChanges() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(Jenkins.ADMINISTER)
.everywhere()
.to("admin"));

WebClient wc = j.createWebClient();
wc.login("admin", "admin");
wc.setThrowExceptionOnFailingStatusCode(false);

WebRequest request1 = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST);
request1.setAdditionalHeader("Content-Type", "application/yaml");
request1.setRequestBody("jenkins:\n systemMessage: 'Idempotency Test'");
var crumbIssuer = j.jenkins.getCrumbIssuer();
if (crumbIssuer != null) {
request1.setAdditionalHeader(
crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null));
}

WebResponse response1 = wc.getPage(request1).getWebResponse();
assertEquals(200, response1.getStatusCode());
assertEquals("Idempotency Test", j.jenkins.getSystemMessage());

WebRequest request2 = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST);
request2.setAdditionalHeader("Content-Type", "application/yaml");
request2.setRequestBody("jenkins:\n systemMessage: 'Idempotency Test'");
if (crumbIssuer != null) {
request2.setAdditionalHeader(
crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null));
}

WebResponse response2 = wc.getPage(request2).getWebResponse();

assertEquals(200, response2.getStatusCode());
assertEquals("Idempotency Test", j.jenkins.getSystemMessage());
}

@Test
public void testDoConfigure_WithApiToken_NoCrumb() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(Jenkins.ADMINISTER)
.everywhere()
.to("admin"));

WebClient wc = j.createWebClient();

wc.withBasicApiToken("admin");
wc.setThrowExceptionOnFailingStatusCode(false);

WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST);
request.setAdditionalHeader("Content-Type", "application/yaml");
request.setRequestBody("jenkins:\n systemMessage: 'API Token Success'");

WebResponse response = wc.getPage(request).getWebResponse();

assertEquals(200, response.getStatusCode());
assertEquals("API Token Success", j.jenkins.getSystemMessage());
}
}
Loading