-
Notifications
You must be signed in to change notification settings - Fork 49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[DRAFT] Update DFIU to verify whether renovate is installed #1133
base: main
Are you sure you want to change the base?
Changes from 3 commits
5ebf634
eaf7cda
1fb97c6
ebbb223
851b05b
53b2886
087f4e3
9685288
2594f38
4a4448a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
package com.salesforce.dockerfileimageupdate.utils; | ||
|
||
import com.auth0.jwt.JWT; | ||
import com.auth0.jwt.algorithms.Algorithm; | ||
import com.salesforce.dockerfileimageupdate.CommandLine; | ||
import net.sourceforge.argparse4j.inf.Namespace; | ||
import org.bouncycastle.util.io.pem.PemReader; | ||
import org.kohsuke.github.GitHub; | ||
import org.kohsuke.github.GitHubBuilder; | ||
import org.kohsuke.github.HttpException; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.io.File; | ||
import java.io.FileReader; | ||
import java.io.IOException; | ||
import java.security.GeneralSecurityException; | ||
import java.security.KeyFactory; | ||
import java.security.Security; | ||
import java.security.interfaces.RSAPrivateKey; | ||
import java.security.spec.PKCS8EncodedKeySpec; | ||
import java.time.Instant; | ||
import java.util.Date; | ||
|
||
public class GithubAppCheck { | ||
private static final Logger log = LoggerFactory.getLogger(GithubAppCheck.class); | ||
|
||
private final String appId; | ||
private final String privateKeyPath; | ||
private String jwt; | ||
private Instant jwtExpiry; | ||
private GitHub gitHub; | ||
|
||
public GithubAppCheck(final Namespace ns){ | ||
this.appId = ns.get(Constants.SKIP_GITHUB_APP_ID); | ||
this.privateKeyPath = ns.get(Constants.SKIP_GITHUB_APP_KEY); | ||
this.jwt = null; | ||
this.jwtExpiry = null; | ||
this.gitHub = null; | ||
if (this.appId != null && this.privateKeyPath != null) { | ||
try { | ||
generateJWT(this.appId, this.privateKeyPath); | ||
} catch (GeneralSecurityException | IOException exception) { | ||
log.warn("Could not initialise JWT due to exception: {}", exception.getMessage()); | ||
} | ||
try { | ||
this.gitHub = new GitHubBuilder() | ||
.withEndpoint(CommandLine.gitApiUrl(ns)) | ||
.withJwtToken(jwt) | ||
.build(); | ||
} catch (IOException exception) { | ||
log.warn("Could not initialise github due to exception: {}", exception.getMessage()); | ||
} | ||
} | ||
else { | ||
log.warn("Could not find any Github app ID and Github app Key in the declared list. Hence assuming this class is no longer needed"); | ||
} | ||
} | ||
|
||
/** | ||
* Method to verify whether the github app is installed on a repository or not. | ||
* @param fullRepoName = The repository full name, i.e, of the format "owner/repoName". Eg: "Salesforce/dockerfile-image-update" | ||
* @return True if github app is installed, false otherwise. | ||
*/ | ||
protected boolean isGithubAppEnabledOnRepository(String fullRepoName){ | ||
refreshJwtIfNeeded(appId, privateKeyPath); | ||
try { | ||
gitHub.getApp().getInstallationByRepository(fullRepoName.split("/")[0], fullRepoName.split("/")[1]); | ||
return true; | ||
} catch (HttpException exception) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're defaulting to false, there's a risk of false negatives. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but I am trying to be cautious. Since we are dealing with docker patches, isn't it better to get both DFIU and Renovate PRs and have a confusing experience, rather than not receive either PR and fall behind on your patches? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree here |
||
if (exception.getResponseCode() != 404) { | ||
// Log for any HTTP status code other than 404 Not found. | ||
log.warn("Caught a HTTPException {} while trying to get app installation. Defaulting to False", exception.getMessage()); | ||
} | ||
return false; | ||
} catch (IOException exception) { | ||
// Most often happens on timeout scenarios. | ||
log.warn("Caught a IOException {} while trying to get app installation. Defaulting to False", exception.getMessage()); | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Method to refresh the JWT token if needed. Checks the JWT expiry time, and if it is 60s away from expiring, refreshes it. | ||
* @param appId = The id of the Github App to generate the JWT for | ||
* @param privateKeyPath = The path to the private key of the Github App to generate the JWT for | ||
*/ | ||
private void refreshJwtIfNeeded(String appId, String privateKeyPath){ | ||
if (jwt == null || jwtExpiry.isBefore(Instant.now().minusSeconds(60))) { // Adding a buffer to ensure token validity | ||
try { | ||
generateJWT(appId, privateKeyPath); | ||
} catch (IOException | GeneralSecurityException exception) { | ||
log.warn("Could not refresh the JWT due to exception: {}", exception.getMessage()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't this cause a problem? If we simply ignore this exception and default the value to false in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above. I am trying to be cautious. Lmk if you think otherwise. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree with Sai in this case |
||
} | ||
} | ||
} | ||
|
||
/** | ||
* Method to generate the JWT used to access the Github App APIs. We generate the JWT to be valid for 600 seconds. | ||
* Along with the JWT value, the jwtExpiry value is set to the time of 600 sec from now. | ||
* @param appId = The id of the Github App to generate the JWT for | ||
* @param privateKeyPath = The path to the private key of the Github App to generate the JWT for | ||
* @throws IOException | ||
* @throws GeneralSecurityException | ||
*/ | ||
private void generateJWT(String appId, String privateKeyPath) throws IOException, GeneralSecurityException { | ||
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); | ||
RSAPrivateKey privateKey = getRSAPrivateKey(privateKeyPath); | ||
|
||
Algorithm algorithm = Algorithm.RSA256(null, privateKey); | ||
Instant now = Instant.now(); | ||
jwt = JWT.create() | ||
.withIssuer(appId) | ||
.withIssuedAt(Date.from(now)) | ||
.withExpiresAt(Date.from(now.plusSeconds(600))) // 10 minutes expiration | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is this hardcoded? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what will the behavior be if you set this really short (for example 1 second) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @afalko we would like to avoid as much API calls as possible. Setting to 1 second means that we have to make an API call to regenerate a new JWT token every 1 second. Taking into account that the DFIU takes hours to run, I believe it is not efficient |
||
.sign(algorithm); | ||
jwtExpiry = now.plusSeconds(600); | ||
} | ||
|
||
/** | ||
* The method to get the private key in an RSA Encoded format. Makes use of org.bouncycastle.util | ||
* @param privateKeyPath | ||
* @return | ||
* @throws IOException | ||
* @throws GeneralSecurityException | ||
*/ | ||
private RSAPrivateKey getRSAPrivateKey(String privateKeyPath) throws IOException, GeneralSecurityException { | ||
try (PemReader pemReader = new PemReader(new FileReader(new File(privateKeyPath)))) { | ||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(pemReader.readPemObject().getContent()); | ||
KeyFactory keyFactory = KeyFactory.getInstance("RSA"); | ||
return (RSAPrivateKey) keyFactory.generatePrivate(spec); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't IDs always integers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@afalko It does not matter as it would be parsed as a parameter in a request API call to Github API to generate a JWT token. We have tested this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not have the parser do the parsing for you upfront here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@afalko the reason for that is because appID will be parsed into the JWT token generation API call under the
.withIssuer(appId)
subcall, and this has to be parsed as a string type. You can see more here: https://www.baeldung.com/java-auth0-jwtWe can change it to
Integer
type if you truly think it is necessary and purposeful (to comply with the App ID integer type from Github App) and convert it to string when parsed into this API callThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha - pass-through might be fine, but it likely be a difficult error message to understand (vs. hey, you need an integer here). I think it is purposeful to make this integer for that release, but not necessary :)