Skip to content

Commit

Permalink
Leverage Project Key property in Cloud Bitbucket API - fetching repos…
Browse files Browse the repository at this point in the history
…itories, pullrequest and push hook processors. (#654)

Co-authored-by: Lukasz Lesiak <lukasz.lesiak@idemia.com>
Co-authored-by: Günter Grodotzki <gunter@grodotzki.com>
  • Loading branch information
3 people committed Mar 20, 2023
1 parent d1aaf82 commit abb9aa5
Show file tree
Hide file tree
Showing 19 changed files with 133 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ public class BitbucketSCMNavigator extends SCMNavigator {
private String credentialsId;
@NonNull
private final String repoOwner;
@CheckForNull
private String projectKey;
@NonNull
private List<SCMTrait<? extends SCMTrait<?>>> traits;
@Deprecated
Expand Down Expand Up @@ -211,6 +213,15 @@ public String getRepoOwner() {
return repoOwner;
}

public String getProjectKey() {
return projectKey;
}

@DataBoundSetter
public void setProjectKey(@CheckForNull String projectKey) {
this.projectKey = Util.fixEmpty(projectKey);
}

@Override
@NonNull
public List<SCMTrait<? extends SCMTrait<?>>> getTraits() {
Expand Down Expand Up @@ -486,7 +497,7 @@ public void visitSources(SCMSourceObserver observer) throws IOException, Interru

BitbucketAuthenticator authenticator = AuthenticationTokens.convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials);

BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null);
BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, projectKey, null);
BitbucketTeam team = bitbucket.getTeam();
if (team != null) {
// Navigate repositories of the team
Expand Down Expand Up @@ -535,7 +546,7 @@ public List<Action> retrieveActions(@NonNull SCMNavigatorOwner owner,

BitbucketAuthenticator authenticator = AuthenticationTokens.convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials);

BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null);
BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null, null);
BitbucketTeam team = bitbucket.getTeam();
if (team != null) {
String defaultTeamUrl;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ public BitbucketApi buildBitbucketClient(PullRequestSCMHead head) {
}

public BitbucketApi buildBitbucketClient(String repoOwner, String repository) {
return BitbucketApiFactory.newInstance(getServerUrl(), authenticator(), repoOwner, repository);
return BitbucketApiFactory.newInstance(getServerUrl(), authenticator(), repoOwner, null, repository);
}

@Override
Expand Down Expand Up @@ -667,6 +667,7 @@ class Skip extends IOException {
getServerUrl(),
authenticator(),
pullRepoOwner,
null,
pullRepository
)
: originBitbucket;
Expand Down Expand Up @@ -1261,7 +1262,7 @@ public ListBoxModel doFillRepositoryItems(@AncestorInPath SCMSourceOwner context
BitbucketAuthenticator authenticator = AuthenticationTokens.convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials);

try {
BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null);
BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null, null);
BitbucketTeam team = bitbucket.getTeam();
List<? extends BitbucketRepository> repositories =
bitbucket.getRepositories(team != null ? null : UserRoleInRepository.CONTRIBUTOR);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ private StandardCredentials findCredentials() {
private AvatarImage doFetch(StandardCredentials credentials) throws IOException, InterruptedException {
BitbucketAuthenticator authenticator = AuthenticationTokens
.convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials);
BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null);
BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null, null);
return bitbucket.getTeamAvatar();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ private static Map<String, String> getTargets(BitbucketSCMSource source) {
source.getServerUrl(),
source.authenticator(),
source.getRepoOwner(),
null,
source.getRepository()
);
for (BitbucketPullRequest pr : bitbucket.getPullRequests()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ public abstract class BitbucketApiFactory implements ExtensionPoint {
* @param serverUrl the server URL.
* @param authenticator the (optional) authenticator.
* @param owner the owner name.
* @param projectKey the (optional) project key.
* @param repository the (optional) repository name.
* @return the {@link BitbucketApi}.
*/
@NonNull
protected abstract BitbucketApi create(@Nullable String serverUrl,
@Nullable BitbucketAuthenticator authenticator,
@NonNull String owner,
@CheckForNull String projectKey,
@CheckForNull String repository);

@NonNull
Expand All @@ -47,7 +49,7 @@ protected BitbucketApi create(@Nullable String serverUrl,
@NonNull String owner,
@CheckForNull String repository) {
BitbucketAuthenticator auth = credentials != null ? new BitbucketUsernamePasswordAuthenticator(credentials) : null;
return create(serverUrl, auth, owner, repository);
return create(serverUrl, auth, owner, null, repository);
}

/**
Expand All @@ -57,6 +59,7 @@ protected BitbucketApi create(@Nullable String serverUrl,
* @param serverUrl the server URL.
* @param authenticator the (optional) authenticator.
* @param owner the owner name.
* @param projectKey the (optional) project key.
* @param repository the (optional) repository name.
* @return the {@link BitbucketApi}.
* @throws IllegalArgumentException if the supplied URL is not supported.
Expand All @@ -65,10 +68,11 @@ protected BitbucketApi create(@Nullable String serverUrl,
public static BitbucketApi newInstance(@Nullable String serverUrl,
@Nullable BitbucketAuthenticator authenticator,
@NonNull String owner,
@CheckForNull String projectKey,
@CheckForNull String repository) {
for (BitbucketApiFactory factory : ExtensionList.lookup(BitbucketApiFactory.class)) {
if (factory.isMatch(serverUrl)) {
return factory.create(serverUrl, authenticator, owner, repository);
return factory.create(serverUrl, authenticator, owner, projectKey, repository);
}
}
throw new IllegalArgumentException("Unsupported Bitbucket server URL: " + serverUrl);
Expand All @@ -79,8 +83,9 @@ public static BitbucketApi newInstance(@Nullable String serverUrl,
public static BitbucketApi newInstance(@Nullable String serverUrl,
@Nullable StandardUsernamePasswordCredentials credentials,
@NonNull String owner,
@CheckForNull String projectKey,
@CheckForNull String repository) {
BitbucketAuthenticator auth = credentials != null ? new BitbucketUsernamePasswordAuthenticator(credentials) : null;
return newInstance(serverUrl, auth, owner, repository);
return newInstance(serverUrl, auth, owner, projectKey, repository);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ public class BitbucketCloudApiClient implements BitbucketApi {
private CloseableHttpClient client;
private HttpClientContext context;
private final String owner;
private final String projectKey;
private final String repositoryName;
private final boolean enableCache;
private final BitbucketAuthenticator authenticator;
Expand Down Expand Up @@ -167,14 +168,15 @@ public static void clearCaches() {
@Deprecated
public BitbucketCloudApiClient(boolean enableCache, int teamCacheDuration, int repositoriesCacheDuration,
String owner, String repositoryName, StandardUsernamePasswordCredentials credentials) {
this(enableCache, teamCacheDuration, repositoriesCacheDuration, owner, repositoryName,
this(enableCache, teamCacheDuration, repositoriesCacheDuration, owner, null, repositoryName,
new BitbucketUsernamePasswordAuthenticator(credentials));
}

public BitbucketCloudApiClient(boolean enableCache, int teamCacheDuration, int repositoriesCacheDuration,
String owner, String repositoryName, BitbucketAuthenticator authenticator) {
String owner, String projectKey, String repositoryName, BitbucketAuthenticator authenticator) {
this.authenticator = authenticator;
this.owner = owner;
this.projectKey = projectKey;
this.repositoryName = repositoryName;
this.enableCache = enableCache;
if (enableCache) {
Expand Down Expand Up @@ -752,9 +754,12 @@ public List<BitbucketCloudRepository> getRepositories(@CheckForNull UserRoleInRe
cacheKey.append("::<anonymous>");
}

final UriTemplate template = UriTemplate.fromTemplate(V2_API_BASE_URL + "{/owner}{?role,page,pagelen}")
final UriTemplate template = UriTemplate.fromTemplate(V2_API_BASE_URL + "{/owner}{?role,page,pagelen,q}")
.set("owner", owner)
.set("pagelen", MAX_PAGE_LENGTH);
if (StringUtils.isNotBlank(projectKey)) {
template.set("q", "project.key=" + "\"" + projectKey + "\""); // q=project.key="<projectKey>"
}
if (role != null && authenticator != null) {
template.set("role", role.getId());
cacheKey.append("::").append(role.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ protected boolean isMatch(@Nullable String serverUrl) {
@NonNull
@Override
protected BitbucketApi create(@Nullable String serverUrl, @Nullable BitbucketAuthenticator authenticator,
@NonNull String owner, @CheckForNull String repository) {
@NonNull String owner, @CheckForNull String projectKey, @CheckForNull String repository) {
AbstractBitbucketEndpoint endpoint = BitbucketEndpointConfiguration.get().findEndpoint(BitbucketCloudEndpoint.SERVER_URL);
boolean enableCache = false;
int teamCacheDuration = 0;
Expand All @@ -33,6 +33,6 @@ protected BitbucketApi create(@Nullable String serverUrl, @Nullable BitbucketAut
}
return new BitbucketCloudApiClient(
enableCache, teamCacheDuration, repositoriesCacheDuration,
owner, repository, authenticator);
owner, projectKey, repository, authenticator);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public SCMFileSystem build(@NonNull SCMSource source, @NonNull SCMHead head, @Ch

BitbucketAuthenticator authenticator = AuthenticationTokens.convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials);

BitbucketApi apiClient = BitbucketApiFactory.newInstance(serverUrl, authenticator, owner, repository);
BitbucketApi apiClient = BitbucketApiFactory.newInstance(serverUrl, authenticator, owner, null, repository);
String ref = null;

if (head instanceof BranchSCMHead) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMSource;
import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy;
import org.apache.commons.lang.StringUtils;

import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_DECLINED;
import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_MERGED;
Expand Down Expand Up @@ -113,12 +114,26 @@ public boolean isMatch(@NonNull SCMNavigator navigator) {
return false;
}
BitbucketSCMNavigator bbNav = (BitbucketSCMNavigator) navigator;
if (!isProjectKeyMatch(bbNav.getProjectKey())) {
return false;
}

if (!isServerUrlMatch(bbNav.getServerUrl())) {
return false;
}
return bbNav.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName());
}

private boolean isProjectKeyMatch(String projectKey) {
if (StringUtils.isBlank(projectKey)) {
return true;
}
if (this.getPayload().getRepository().getProject() != null) {
return projectKey.equals(this.getPayload().getRepository().getProject().getKey());
}
return true;
}

private boolean isServerUrlMatch(String serverUrl) {
if (serverUrl == null || BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl)) {
// this is a Bitbucket cloud navigator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import jenkins.scm.api.SCMNavigator;
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMSource;
import org.apache.commons.lang.StringUtils;

public class PushHookProcessor extends HookProcessor {

Expand Down Expand Up @@ -94,12 +95,25 @@ public boolean isMatch(@NonNull SCMNavigator navigator) {
return false;
}
BitbucketSCMNavigator bbNav = (BitbucketSCMNavigator) navigator;
if (!isProjectKeyMatch(bbNav.getProjectKey())) {
return false;
}
if (!isServerUrlMatch(bbNav.getServerUrl())) {
return false;
}
return bbNav.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName());
}

private boolean isProjectKeyMatch(String projectKey) {
if (StringUtils.isBlank(projectKey)) {
return true;
}
if (this.getPayload().getRepository().getProject() != null) {
return projectKey.equals(this.getPayload().getRepository().getProject().getKey());
}
return true;
}

private boolean isServerUrlMatch(String serverUrl) {
if (serverUrl == null || BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl)) {
// this is a Bitbucket cloud navigator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ private BitbucketApi bitbucketApiFor(BitbucketSCMSource source) {
endpoint.getServerUrl(),
endpoint.authenticator(),
source.getRepoOwner(),
null,
source.getRepository()
);
case ITEM:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ protected boolean isMatch(@Nullable String serverUrl) {
@NonNull
@Override
protected BitbucketApi create(@Nullable String serverUrl, @Nullable BitbucketAuthenticator authenticator,
@NonNull String owner, @CheckForNull String repository) {
@NonNull String owner, @CheckForNull String projectKey, @CheckForNull String repository) {
if(StringUtils.isBlank(serverUrl)){
throw new IllegalArgumentException("serverUrl is required");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
<f:entry title="${%Owner}" field="repoOwner">
<f:textbox/>
</f:entry>
<f:entry title="${%Project Key}" field="projectKey">
<f:textbox/>
</f:entry>
<f:entry title="${%Behaviours}" field="traits">
<scm:traits field="traits"/>
</f:entry>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
(Optionally) If you're using <strong>Bitbucket Cloud</strong> specify project key to select only repositories belonging to that project.
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ protected boolean isMatch(@Nullable String serverUrl) {
@NonNull
@Override
protected BitbucketApi create(@Nullable String serverUrl, @Nullable BitbucketAuthenticator authenticator,
@NonNull String owner, @CheckForNull String repository) {
@NonNull String owner, @CheckForNull String projectKey, @CheckForNull String repository) {
return mocks.get(StringUtils.defaultString(serverUrl, NULL));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,52 @@ public void basic_cloud() throws Exception {
assertThat(instance.getExcludes(), is(""));
}

@Test
public void cloud_project_key() throws Exception {
BitbucketSCMNavigator instance = load();
assertThat(instance.id(), is("https://bitbucket.org::cloudbeers"));
assertThat(instance.getRepoOwner(), is("cloudbeers"));
assertThat(instance.getProjectKey(), is("PK"));
assertThat(instance.getServerUrl(), is(BitbucketCloudEndpoint.SERVER_URL));
assertThat(instance.getCredentialsId(), is("bcaef157-f105-407f-b150-df7722eab6c1"));
assertThat("SAME checkout credentials should mean no checkout trait",
instance.getTraits(),
not(hasItem(instanceOf(SSHCheckoutTrait.class))));
assertThat(".* as a pattern should mean no RegexSCMSourceFilterTrait",
instance.getTraits(),
not(hasItem(instanceOf(RegexSCMSourceFilterTrait.class))));
assertThat(instance.getTraits(),
containsInAnyOrder(
allOf(
instanceOf(BranchDiscoveryTrait.class),
hasProperty("buildBranch", is(true)),
hasProperty("buildBranchesWithPR", is(true))
),
allOf(
instanceOf(OriginPullRequestDiscoveryTrait.class),
hasProperty("strategyId", is(2))
),
allOf(
instanceOf(ForkPullRequestDiscoveryTrait.class),
hasProperty("strategyId", is(2)),
hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class))
),
instanceOf(PublicRepoPullRequestFilterTrait.class),
allOf(
instanceOf(WebhookRegistrationTrait.class),
hasProperty("mode", is(WebhookRegistration.DISABLE))
)
)
);
// legacy API
assertThat(instance.getBitbucketServerUrl(), is(nullValue()));
assertThat(instance.getCheckoutCredentialsId(), is("SAME"));
assertThat(instance.getPattern(), is(".*"));
assertThat(instance.isAutoRegisterHooks(), is(false));
assertThat(instance.getIncludes(), is("*"));
assertThat(instance.getExcludes(), is(""));
}

@Test
public void basic_server() throws Exception {
BitbucketSCMNavigator instance = load();
Expand Down
Loading

0 comments on commit abb9aa5

Please sign in to comment.