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,162 @@
package io.jenkins.plugins.casc;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;

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.MockAuthorizationStrategy;

public class ConfigurationAsCodeApiTest {

@Rule
public JenkinsRule j = new JenkinsRule();

private static final String ENDPOINT = "configuration-as-code/configure";
private static final String YAML_CONTENT_TYPE = "application/yaml";
private static final String ADMIN = "admin";

private WebRequest webRequest(HttpMethod method) throws Exception {
return new WebRequest(new URL(j.getURL(), ENDPOINT), method);
}

private WebRequest yamlPost(String requestBody) throws Exception {
WebRequest request = webRequest(HttpMethod.POST);
request.setAdditionalHeader("Content-Type", YAML_CONTENT_TYPE);
if (requestBody != null) {
request.setRequestBody(requestBody);
}
return request;
}

private void configureAdminSecurity() {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(Jenkins.ADMINISTER)
.everywhere()
.to(ADMIN));
}

@Test
public void testDoConfigure_RequiresPost() throws Exception {
try (JenkinsRule.WebClient wc = j.createWebClient()) {
wc.setThrowExceptionOnFailingStatusCode(false);

WebResponse response = wc.getPage(webRequest(HttpMethod.GET)).getWebResponse();
assertThat(response.getStatusCode(), is(405));
}
}

@Test
public void testDoConfigure_Success() throws Exception {
configureAdminSecurity();

try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) {
wc.setThrowExceptionOnFailingStatusCode(false);

WebResponse response = wc.getPage(yamlPost("jenkins:\n systemMessage: 'Webhook Success'"))
.getWebResponse();

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

@Test
public void testDoConfigure_InvalidYaml() throws Exception {
configureAdminSecurity();

try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) {
wc.setThrowExceptionOnFailingStatusCode(false);

WebResponse response =
wc.getPage(yamlPost("jenkins:\n systemMessage: [invalid")).getWebResponse();

assertThat(response.getStatusCode(), is(400));
assertThat(response.getContentAsString(), containsString("message"));
}
}

@Test
public void testDoConfigure_NonAdminForbidden() throws Exception {
try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken("user")) {
wc.setThrowExceptionOnFailingStatusCode(false);

WebRequest request = yamlPost("jenkins:\n systemMessage: 'fail'");

WebResponse response = wc.getPage(request).getWebResponse();
assertThat(response.getStatusCode(), is(403));
}
}

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

try (JenkinsRule.WebClient wc = j.createWebClient()) {
wc.setThrowExceptionOnFailingStatusCode(false);

WebRequest request = yamlPost("jenkins:\n systemMessage: 'anonymous bypass attempt'");

WebResponse response = wc.getPage(request).getWebResponse();
assertThat(response.getStatusCode(), is(403));
}
}

@Test
public void testDoConfigure_EmptyBody() throws Exception {
configureAdminSecurity();

try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) {
wc.setThrowExceptionOnFailingStatusCode(false);

WebResponse response = wc.getPage(yamlPost(null)).getWebResponse();

assertThat(response.getStatusCode(), is(400));
assertThat(response.getContentAsString(), containsString("message"));
}
}

@Test
public void testDoConfigure_MalformedStructure() throws Exception {
configureAdminSecurity();

try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) {
wc.setThrowExceptionOnFailingStatusCode(false);

WebResponse response = wc.getPage(yamlPost("jenkins:\n invalidRoot:\n foo: bar"))
.getWebResponse();

assertThat(response.getStatusCode(), is(400));
assertThat(response.getContentAsString(), containsString("message"));
}
}

@Test
public void testDoConfigure_ValidYaml_NoChanges() throws Exception {
configureAdminSecurity();

try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) {
wc.setThrowExceptionOnFailingStatusCode(false);

WebResponse response1 = wc.getPage(yamlPost("jenkins:\n systemMessage: 'Idempotency Test'"))
.getWebResponse();
assertThat(response1.getStatusCode(), is(200));
assertThat(j.jenkins.getSystemMessage(), is("Idempotency Test"));

WebResponse response2 = wc.getPage(yamlPost("jenkins:\n systemMessage: 'Idempotency Test'"))
.getWebResponse();
assertThat(response2.getStatusCode(), is(200));
assertThat(j.jenkins.getSystemMessage(), is("Idempotency Test"));
}
}
}
Loading