Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,33 @@ It starts with the prefix `SCMM_CUSTOM_PROP_` and will end with the key of the c
For example the custom property with the key `lang`, will be injected as the environment variable `SCMM_CUSTOM_PROP_lang`.
The value of the environment variable, will be the value of the custom property.

#### Pull Request Labels

For pull request builds, SCM-Manager pull request labels are injected as environment variables.
`SCMM_PR_LABELS` contains all labels as a JSON array, preserving the exact label values and order from SCM-Manager.
For simple access, the plugin also injects `SCMM_PR_LABEL_COUNT` and numbered variables `SCMM_PR_LABEL_1`, `SCMM_PR_LABEL_2`, and so on.

Example:

```groovy
pipeline {
agent any

stages {
stage('Labels') {
steps {
script {
def labels = new groovy.json.JsonSlurperClassic().parseText(env.SCMM_PR_LABELS ?: '[]')
if (labels.contains('backend')) {
echo 'Backend label is set'
}
}
}
}
}
}
```

### Organization Folders – Namespaces
If you want to have build jobs for every repository in a namespace, you can create "SCM-Manager Namespace" jobs. These
will scan all repositories in the given namespace and create multibranch pipelines for each repository where a
Expand Down
27 changes: 27 additions & 0 deletions docs/de/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,33 @@ Das "Custom Properties" Plugin unterstützt auch, dass mehrere Werte für eine E
Hierbei werden die Werte mittels Tab bzw. einem `\t` separiert.
Die Werte einer Eigenschaft werden in der jeweiligen Umgebungsvariable ebenfalls per Tab separiert gesetzt.

#### Pull-Request-Labels

Bei Pull-Request-Builds werden Labels des SCM-Manager Pull Requests als Umgebungsvariablen gesetzt.
`SCMM_PR_LABELS` enthält alle Labels als JSON-Array und erhält damit die exakten Werte und die Reihenfolge aus dem SCM-Manager.
Für einfachen Zugriff werden zusätzlich `SCMM_PR_LABEL_COUNT` und durchnummerierte Variablen `SCMM_PR_LABEL_1`, `SCMM_PR_LABEL_2` usw. gesetzt.

Beispiel:

```groovy
pipeline {
agent any

stages {
stage('Labels') {
steps {
script {
def labels = new groovy.json.JsonSlurperClassic().parseText(env.SCMM_PR_LABELS ?: '[]')
if (labels.contains('backend')) {
echo 'Backend-Label ist gesetzt'
}
}
}
}
}
}
```

### Organization Folders – Namespaces
Sollen für alle Repositorys eines **kompletten Namespaces** im SCM-Manager Jobs erzeugt werden, kann ein **Organization Folder**-Job mit einem SCM-Manager-Namespace als Quelle genutzt werden.
Dieser prüft alle Repositorys in einem gegebenen Namespace und erzeugt entsprechende Multibranch-Pipelines, wenn im Wurzelverzeichnis des Repositorys eine `Jenkinsfile` gefunden wurde.
Expand Down
18 changes: 16 additions & 2 deletions src/main/java/com/cloudogu/scmmanager/scm/api/PullRequest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.cloudogu.scmmanager.scm.api;

import de.otto.edison.hal.HalRepresentation;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public class PullRequest extends HalRepresentation implements ScmManagerObservable {
Expand All @@ -11,6 +13,8 @@

private String target;

private List<String> labels = Collections.emptyList();

private CloneInformation cloneInformation;

private Branch sourceBranch;
Expand Down Expand Up @@ -53,14 +57,22 @@
return target;
}

public List<String> getLabels() {
if (labels == null) {

Check warning on line 61 in src/main/java/com/cloudogu/scmmanager/scm/api/PullRequest.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 61 is only partially covered, one branch is missing
return Collections.emptyList();

Check warning on line 62 in src/main/java/com/cloudogu/scmmanager/scm/api/PullRequest.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 62 is not covered by tests
}
return Collections.unmodifiableList(labels);
}

@Override
public ScmManagerPullRequestHead head() {
if (head == null) {
head = new ScmManagerPullRequestHead(
cloneInformation,
id,
new ScmManagerHead(cloneInformation, target),
new ScmManagerHead(cloneInformation, source));
new ScmManagerHead(cloneInformation, source),
getLabels());
}
return head;
}
Expand All @@ -79,14 +91,16 @@
return Objects.equals(id, that.id)
&& Objects.equals(source, that.source)
&& Objects.equals(target, that.target)
&& Objects.equals(getLabels(), that.getLabels())
&& Objects.equals(cloneInformation, that.cloneInformation)
&& Objects.equals(sourceBranch, that.sourceBranch)
&& Objects.equals(targetBranch, that.targetBranch)
&& Objects.equals(head, that.head);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), id, source, target, cloneInformation, sourceBranch, targetBranch, head);
return Objects.hash(
super.hashCode(), id, source, target, getLabels(), cloneInformation, sourceBranch, targetBranch, head);

Check warning on line 104 in src/main/java/com/cloudogu/scmmanager/scm/api/PullRequest.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 94-104 are not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.cloudogu.scmmanager.scm.api;

import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import jenkins.scm.api.SCMHeadOrigin;
import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy;
import jenkins.scm.api.mixin.ChangeRequestSCMHead2;
Expand All @@ -14,17 +17,28 @@

private final ScmManagerHead target;
private final ScmManagerHead source;
private final List<String> labels;

public ScmManagerPullRequestHead(
@NonNull CloneInformation cloneInformation,
@NonNull String id,
@NonNull ScmManagerHead target,
ScmManagerHead source) {
this(cloneInformation, id, target, source, Collections.emptyList());
}

public ScmManagerPullRequestHead(
@NonNull CloneInformation cloneInformation,
@NonNull String id,
@NonNull ScmManagerHead target,
ScmManagerHead source,
List<String> labels) {
// ?? why PullRequest/...
super(cloneInformation, "PR-" + id);
this.id = id;
this.target = target;
this.source = source;
this.labels = labels == null ? Collections.emptyList() : new ArrayList<>(labels);

Check warning on line 41 in src/main/java/com/cloudogu/scmmanager/scm/api/ScmManagerPullRequestHead.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 41 is only partially covered, one branch is missing
}

@NonNull
Expand All @@ -43,6 +57,13 @@
return source;
}

public List<String> getLabels() {
if (labels == null) {

Check warning on line 61 in src/main/java/com/cloudogu/scmmanager/scm/api/ScmManagerPullRequestHead.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 61 is only partially covered, one branch is missing
return Collections.emptyList();

Check warning on line 62 in src/main/java/com/cloudogu/scmmanager/scm/api/ScmManagerPullRequestHead.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 62 is not covered by tests
}
return Collections.unmodifiableList(labels);
}

@NonNull
@Override
public ChangeRequestCheckoutStrategy getCheckoutStrategy() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.cloudogu.scmmanager.scm.env;

import com.cloudogu.scmmanager.scm.ScmManagerApiData;
import com.cloudogu.scmmanager.scm.api.PullRequest;
import com.cloudogu.scmmanager.scm.api.Repository;
import com.cloudogu.scmmanager.scm.api.ScmManagerApi;
import com.cloudogu.scmmanager.scm.api.ScmManagerApiFactory;
import com.cloudogu.scmmanager.scm.api.ScmManagerPullRequestHead;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import hudson.EnvVars;
import hudson.Extension;
import hudson.model.EnvironmentContributor;
import hudson.model.ItemGroup;
import hudson.model.Job;
import hudson.model.Run;
import hudson.model.TaskListener;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nonnull;
import jenkins.branch.Branch;
import jenkins.scm.api.SCMHead;
import lombok.extern.slf4j.Slf4j;
import org.jenkinsci.plugins.workflow.multibranch.BranchJobProperty;

@Extension(optional = true)
@Slf4j
public class PullRequestLabelsEnvContributor extends EnvironmentContributor {

private static final String ENV_LABELS = "SCMM_PR_LABELS";
private static final String ENV_LABEL_COUNT = "SCMM_PR_LABEL_COUNT";
private static final String ENV_LABEL_PREFIX = "SCMM_PR_LABEL_";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

private final ScmManagerApiFactory apiFactory;

public PullRequestLabelsEnvContributor() {
this(new ScmManagerApiFactory());
}

Check warning on line 40 in src/main/java/com/cloudogu/scmmanager/scm/env/PullRequestLabelsEnvContributor.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 39-40 are not covered by tests

PullRequestLabelsEnvContributor(ScmManagerApiFactory apiFactory) {
this.apiFactory = apiFactory;
}

@Override
public void buildEnvironmentFor(@Nonnull Run run, @Nonnull EnvVars envs, @Nonnull TaskListener listener) {
ScmManagerPullRequestHead pullRequestHead = getPullRequestHead(run);
if (pullRequestHead == null) {
return;
}

List<String> labels = fetchLabels(run, pullRequestHead);
envs.put(ENV_LABELS, toJson(labels));
envs.put(ENV_LABEL_COUNT, String.valueOf(labels.size()));

for (int i = 0; i < labels.size(); i++) {
envs.put(ENV_LABEL_PREFIX + (i + 1), labels.get(i));
}
}

private ScmManagerPullRequestHead getPullRequestHead(Run run) {
Job<?, ?> parent = run.getParent();
BranchJobProperty branchJobProperty = parent.getProperty(BranchJobProperty.class);
if (branchJobProperty == null) {
return null;
}

Branch branch = branchJobProperty.getBranch();
SCMHead head = branch.getHead();
if (head instanceof ScmManagerPullRequestHead) {
return (ScmManagerPullRequestHead) head;
}

return null;
}

private List<String> fetchLabels(Run run, ScmManagerPullRequestHead pullRequestHead) {
Job<?, ?> job = run.getParent();
ScmManagerApiData apiData = job.getAction(ScmManagerApiData.class);
if (apiData == null) {
return pullRequestHead.getLabels();
}

try {
ScmManagerApi client = apiFactory.create(
(ItemGroup<?>) job.getParent(), apiData.getServerUrl(), apiData.getCredentialsId());
Repository repository = client.getRepository(apiData.getNamespace(), apiData.getName())
.get();
CompletableFuture<PullRequest> pullRequest = client.getPullRequest(repository, pullRequestHead.getId());
if (pullRequest == null) {

Check warning on line 91 in src/main/java/com/cloudogu/scmmanager/scm/env/PullRequestLabelsEnvContributor.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 91 is only partially covered, one branch is missing
return pullRequestHead.getLabels();

Check warning on line 92 in src/main/java/com/cloudogu/scmmanager/scm/env/PullRequestLabelsEnvContributor.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 92 is not covered by tests
}
return pullRequest.get().getLabels();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("could not fetch labels for pull request {}. Error: {}", pullRequestHead.getId(), e.getMessage());
} catch (ExecutionException e) {
log.error("could not fetch labels for pull request {}. Error: {}", pullRequestHead.getId(), e.getMessage());
}

return pullRequestHead.getLabels();

Check warning on line 102 in src/main/java/com/cloudogu/scmmanager/scm/env/PullRequestLabelsEnvContributor.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 95-102 are not covered by tests
}

private String toJson(List<String> labels) {
try {
return OBJECT_MAPPER.writeValueAsString(labels);
} catch (JsonProcessingException e) {
log.warn("could not serialize SCM-Manager pull request labels", e);
return "[]";

Check warning on line 110 in src/main/java/com/cloudogu/scmmanager/scm/env/PullRequestLabelsEnvContributor.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 108-110 are not covered by tests
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.cloudogu.scmmanager.scm;

import static org.assertj.core.api.Assertions.assertThat;

import java.net.URL;
import java.util.List;
import jenkins.model.Jenkins;
import okhttp3.mockwebserver.MockWebServer;
import org.htmlunit.HttpMethod;
import org.htmlunit.Page;
import org.htmlunit.WebRequest;
import org.htmlunit.util.NameValuePair;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;

@WithJenkins
class DescriptorEndpointSecurityTest {

private static final String LOW_PRIVILEGE_USER = "lowpriv";
private static final String NAVIGATOR_DESCRIPTOR =
"descriptorByName/com.cloudogu.scmmanager.scm.ScmManagerNavigator";
private static final String SOURCE_DESCRIPTOR = "descriptorByName/com.cloudogu.scmmanager.scm.ScmManagerSource";

private JenkinsRule j;

@BeforeEach
void beforeEach(JenkinsRule rule) {
j = rule;
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(Jenkins.READ)
.everywhere()
.to(LOW_PRIVILEGE_USER)
.grant(Jenkins.ADMINISTER)
.everywhere()
.to("admin"));
}

@Test
void shouldRequirePostForServerUrlChecks() throws Exception {
assertGetIsRejected(NAVIGATOR_DESCRIPTOR + "/checkServerUrl?value=http://example.com");
assertGetIsRejected(SOURCE_DESCRIPTOR + "/checkServerUrl?value=http://example.com");
}

@Test
void shouldSkipLowPrivilegeCredentialChecksBeforeConnectingToServerUrl() throws Exception {
try (MockWebServer server = new MockWebServer()) {
server.start();
String serverUrl = server.url("/").toString();

assertPostIsOk(
NAVIGATOR_DESCRIPTOR + "/checkCredentialsId",
List.of(new NameValuePair("serverUrl", serverUrl), new NameValuePair("value", "scm-creds")));
assertPostIsOk(
SOURCE_DESCRIPTOR + "/checkCredentialsId",
List.of(new NameValuePair("serverUrl", serverUrl), new NameValuePair("value", "scm-creds")));

assertThat(server.getRequestCount()).isZero();
}
}

private void assertGetIsRejected(String path) throws Exception {
JenkinsRule.WebClient webClient = lowPrivilegeWebClient();

Page page = webClient.goTo(path, "text/html");

assertThat(page.getWebResponse().getStatusCode()).isEqualTo(405);
}

private void assertPostIsOk(String path, List<NameValuePair> parameters) throws Exception {
JenkinsRule.WebClient webClient = lowPrivilegeWebClient();
WebRequest request = new WebRequest(new URL(j.getURL(), path), HttpMethod.POST);
request.setRequestParameters(parameters);

Page page = webClient.getPage(webClient.addCrumb(request));

assertThat(page.getWebResponse().getStatusCode()).isEqualTo(200);
}

private JenkinsRule.WebClient lowPrivilegeWebClient() {
return j.createWebClient()
.withBasicCredentials(LOW_PRIVILEGE_USER, LOW_PRIVILEGE_USER)
.withThrowExceptionOnFailingStatusCode(false);
}
}
Loading