Skip to content

Commit

Permalink
Remove the unsupported Nodes GraphQL library
Browse files Browse the repository at this point in the history
The library that had been used to integrate with GitHub's GraphQL APIs
is no longer maintained. As the rest of the project uses Rest APIs to
integrate with services, there's a fairly mature GitHub Java library,
and switching to any other GraphQL library is likely to require as much
effort as switching to a Rest implementation, this change is removing
the GraphQL implementation and moving to the github-api library bundled
within Sonarqube. As GitHub's Rest API does not support minimising
comments, old summary comments are being deleted rather than minimised
after a new summary comment is added. Additionally, the 'bridging'
features used in the github-api library cause issues when mockito
attempts to mock/spy a bridged class, so an unbridged version of the
library has been specified at the start of the test runtime classpath to
allow unit testing using the affected classes.

Includes an upgrade to the docker-compose file to use a newer Postgres
version and resolve some linting issues.
  • Loading branch information
mc1arke committed Nov 13, 2024
1 parent 3d1e938 commit fd5d52e
Show file tree
Hide file tree
Showing 59 changed files with 653 additions and 3,114 deletions.
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
configurations {
zip
customTestRuntime
}

compileJava {
Expand All @@ -60,14 +61,13 @@ tasks.withType(JavaCompile) {


dependencies {
customTestRuntime('org.kohsuke:github-api-unbridged:1.326')
compileOnly(fileTree(dir: sonarLibraries, include: '**/*.jar', exclude: 'extensions/*.jar'))
testImplementation(fileTree(dir: sonarLibraries, include: '**/*.jar', exclude: 'extensions/*.jar'))
testImplementation('org.mockito:mockito-core:5.14.2')
testImplementation('org.assertj:assertj-core:3.26.3')
testImplementation('org.wiremock:wiremock:3.9.2')
zip("sonarqube:sonarqube:${sonarqubeVersion}@zip")
implementation('org.bouncycastle:bcpkix-jdk15on:1.70')
implementation(files('lib/nodes-0.5.0.jar'))
runtimeOnly('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.1')
compileOnly('com.google.code.findbugs:jsr305:3.0.2')
implementation('org.javassist:javassist:3.30.2-GA')
Expand All @@ -78,6 +78,7 @@ dependencies {
testRuntimeOnly('org.junit.vintage:junit-vintage-engine')
}

sourceSets.test.runtimeClasspath = configurations.customTestRuntime + sourceSets.test.runtimeClasspath

project.afterEvaluate {
if (file("${sonarLibraries}").exists()) {
Expand Down
15 changes: 10 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
version: "3.8"

services:
sonarqube:
depends_on:
- db
db:
condition: service_healthy
image: mc1arke/sonarqube-with-community-branch-plugin:${SONARQUBE_VERSION}
build:
context: .
Expand All @@ -13,7 +12,7 @@ services:
PLUGIN_VERSION: ${PLUGIN_VERSION}
container_name: sonarqube
ports:
- 9000:9000
- "9000:9000"
networks:
- sonarnet
environment:
Expand All @@ -24,7 +23,13 @@ services:
- sonarqube_conf:/opt/sonarqube/conf
- sonarqube_data:/opt/sonarqube/data
db:
image: postgres:11
image: postgres:16
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U sonar" ]
interval: 10s
timeout: 5s
retries: 5
hostname: db
container_name: postgres
networks:
- sonarnet
Expand Down
Binary file removed lib/nodes-0.5.0.jar
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2022 Michael Clarke
* Copyright (C) 2020-2024 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
Expand All @@ -18,15 +18,21 @@
*/
package com.github.mc1arke.sonarqube.plugin;

import com.github.mc1arke.sonarqube.plugin.almclient.DefaultLinkHeaderReader;
import org.sonar.api.CoreProperties;
import org.sonar.api.Plugin;
import org.sonar.api.PropertyType;
import org.sonar.api.SonarQubeSide;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.resources.Qualifiers;
import org.sonar.core.config.PurgeConstants;
import org.sonar.core.extension.CoreExtension;

import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.DefaultAzureDevopsClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.DefaultBitbucketClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.HttpClientBuilderFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.github.DefaultGithubClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.DefaultUrlConnectionProvider;
import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.RestApplicationAuthenticationProvider;
import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.DefaultGraphqlProvider;
import com.github.mc1arke.sonarqube.plugin.almclient.github.GithubClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.DefaultGitlabClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.DefaultLinkHeaderReader;
import com.github.mc1arke.sonarqube.plugin.ce.CommunityReportAnalysisComponentProvider;
import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory;
import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchConfigurationLoader;
Expand Down Expand Up @@ -58,15 +64,6 @@
import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.pullrequest.action.DeleteAction;
import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.pullrequest.action.ListAction;

import org.sonar.api.CoreProperties;
import org.sonar.api.Plugin;
import org.sonar.api.PropertyType;
import org.sonar.api.SonarQubeSide;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.resources.Qualifiers;
import org.sonar.core.config.PurgeConstants;
import org.sonar.core.extension.CoreExtension;

/**
* @author Michael Clarke
*/
Expand Down Expand Up @@ -97,11 +94,8 @@ public void load(CoreExtension.Context context) {
PullRequestWs.class,

GithubValidator.class,
DefaultGraphqlProvider.class,
DefaultGithubClientFactory.class,
GithubClientFactory.class,
DefaultLinkHeaderReader.class,
DefaultUrlConnectionProvider.class,
RestApplicationAuthenticationProvider.class,
HttpClientBuilderFactory.class,
DefaultBitbucketClientFactory.class,
BitbucketValidator.class,
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Michael Clarke
* Copyright (C) 2021-2024 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
Expand All @@ -18,11 +18,102 @@
*/
package com.github.mc1arke.sonarqube.plugin.almclient.github;

import java.io.IOException;
import java.io.StringReader;
import java.security.PrivateKey;
import java.time.Clock;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Optional;
import java.util.function.Supplier;

import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.kohsuke.github.GHAppInstallationToken;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.config.internal.Settings;
import org.sonar.api.server.ServerSide;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import org.springframework.beans.factory.annotation.Autowired;

import com.github.mc1arke.sonarqube.plugin.InvalidConfigurationException;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.DefaultJwtBuilder;
import okhttp3.OkHttpClient;

@ServerSide
@ComputeEngineSide
public class GithubClientFactory {

private final Clock clock;
private final Settings settings;
private final Supplier<GitHubBuilder> gitHubBuilderSupplier;

@Autowired
public GithubClientFactory(Clock clock, Settings settings) {
this(clock, settings, GitHubBuilder::new);
}

GithubClientFactory(Clock clock, Settings settings, Supplier<GitHubBuilder> gitHubBuilderSupplier) {
this.clock = clock;
this.settings = settings;
this.gitHubBuilderSupplier = gitHubBuilderSupplier;
}

public GitHub createClient(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) throws IOException {
GHAppInstallationToken repositoryAuthenticationToken = authenticate(projectAlmSettingDto, almSettingDto);

return gitHubBuilderSupplier.get()
.withConnector(new OkHttpGitHubConnector(new OkHttpClient()))
.withEndpoint(almSettingDto.getUrl())
.withAppInstallationToken(repositoryAuthenticationToken.getToken())
.build();
}

private GHAppInstallationToken authenticate(ProjectAlmSettingDto projectAlmSettingDto, AlmSettingDto almSettingDto) {
String apiUrl = Optional.ofNullable(almSettingDto.getUrl()).orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.GLOBAL, "No URL has been set for Github connections"));
String apiPrivateKey = Optional.ofNullable(almSettingDto.getDecryptedPrivateKey(settings.getEncryption())).orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.GLOBAL, "No private key has been set for Github connections"));
String projectPath = Optional.ofNullable(projectAlmSettingDto.getAlmRepo()).orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "No repository name has been set for Github connections"));
String appId = Optional.ofNullable(almSettingDto.getAppId()).orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.GLOBAL, "No App ID has been set for Github connections"));

try {
return getInstallationToken(apiUrl, appId, apiPrivateKey, projectPath);
} catch (IOException ex) {
throw new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "Could not create Github client - " + ex.getMessage(), ex);
}

}

private GHAppInstallationToken getInstallationToken(String apiUrl, String appId, String apiPrivateKey, String projectPath) throws IOException {
Instant issued = clock.instant().minus(10, ChronoUnit.SECONDS);
Instant expiry = issued.plus(2, ChronoUnit.MINUTES);
String jwtToken = new DefaultJwtBuilder().issuedAt(Date.from(issued)).expiration(Date.from(expiry))
.claim("iss", appId).signWith(createPrivateKey(apiPrivateKey), Jwts.SIG.RS256).compact();

public interface GithubClientFactory {
if (!projectPath.contains("/")) {
throw new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "Repository name must be in the format 'owner/repo'");
}
String owner = projectPath.split("/")[0];
String repo = projectPath.split("/")[1];
GitHub github = gitHubBuilderSupplier.get()
.withEndpoint(apiUrl)
.withConnector(new OkHttpGitHubConnector(new OkHttpClient()))
.withJwtToken(jwtToken)
.build();

GithubClient createClient(ProjectAlmSettingDto projectAlmSettingDto, AlmSettingDto almSettingDto);
return github.getApp().getInstallationByRepository(owner, repo).createToken().create();
}

private static PrivateKey createPrivateKey(String apiPrivateKey) throws IOException {
try (PEMParser pemParser = new PEMParser(new StringReader(apiPrivateKey))) {
return new JcaPEMKeyConverter().getPrivateKey(((PEMKeyPair) Optional.ofNullable(pemParser.readObject()).orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.GLOBAL, "Private key could not be parsed"))).getPrivateKeyInfo());
}
}
}
Loading

0 comments on commit fd5d52e

Please sign in to comment.