Skip to content

[JENKINS-62249] Delegate GitHub App token generation from agents and improve caching #326

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

Merged
merged 19 commits into from
Sep 1, 2020

Conversation

bitwiseman
Copy link
Contributor

@bitwiseman bitwiseman commented Aug 14, 2020

Description

Makes GitHub App credentials more stable by requesting them less often and only requesting them via master.

The code was treating tokens as expired after 4 minutes on the assumption they were only valid for 8 minutes. They are actually valid for 60 minutes. It is safe to continue to return them for at least 15 minutes, leaving 45 minutes of valid use.

(NOTE: this PR does not address cases where individual operations take more than 45 minutes.)

It appears that a number of users have experienced network issues around agents requesting tokens directly from GitHub.
This change makes agents ask the controller to for updated tokens, rather than trying to get them directly.

Requesting new tokens less often and routing the requests through the controller mitigates most cases of checkout failing due to "socket timeout".

See
JENKINS-62249 for further information.

Submitter checklist

  • Link to JIRA ticket in description, if appropriate.
  • Change is code complete and matches issue description
  • Automated tests have been added to exercise the changes
  • Reviewer's manual test instructions provided in PR description. See Reviewer's first task below.

Reviewer checklist

  • Run the changes and verify that the change matches the issue description
  • Reviewed the code
  • Verified that the appropriate tests have been written or valid explanation given

Documentation changes

  • Link to jenkins.io PR, or an explanation for why no doc changes are needed

Users/aliases to notify

@bitwiseman bitwiseman marked this pull request as draft August 17, 2020 15:11
appInstallationToken = generateAppInstallationToken(appID, privateKey.getPlainText(), apiUri, owner);
cachedToken = appInstallationToken;
tokenCacheTime = now;
synchronized (this) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If multiple requests for a password are made concurrently, we want to just get one refreshed token.

@jglick
Copy link
Member

jglick commented Aug 20, 2020

Amends #302.

@bitwiseman bitwiseman force-pushed the issue/appid-expiration branch 2 times, most recently from 8244fde to 1136fca Compare August 24, 2020 17:21
@bitwiseman bitwiseman force-pushed the issue/appid-expiration branch from 1136fca to 8367aeb Compare August 24, 2020 17:23
@@ -104,7 +111,7 @@ public void setOwner(String owner) {
}

@SuppressWarnings("deprecation") // preview features are required for GitHub app integration, GitHub api adds deprecated to all preview methods
static String generateAppInstallationToken(String appId, String appPrivateKey, String apiUrl, String owner) {
static AppInstallationToken generateAppInstallationToken(String appId, String appPrivateKey, String apiUrl, String owner) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of returning the bare token, return a serializable instance of the token and when it should be refreshed.

@jglick
Copy link
Member

jglick commented Aug 24, 2020

Subsumes #330.

@bitwiseman bitwiseman force-pushed the issue/appid-expiration branch from 5aea4eb to b76dbdb Compare August 25, 2020 01:06
@bitwiseman bitwiseman marked this pull request as ready for review August 25, 2020 01:21
@bitwiseman bitwiseman requested review from dwnusbaum and jtnord August 25, 2020 01:21
@bitwiseman bitwiseman changed the title Delegate GitHub App token generation from agents and improve caching [JENKINS-62249] Delegate GitHub App token generation from agents and improve caching Aug 25, 2020
Copy link
Member

@dwnusbaum dwnusbaum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks ok to me. Would it be relatively easy to add WireMock tests for this behavior, especially with a token being used on an agent, or not really?

@bitwiseman
Copy link
Contributor Author

@dwnusbaum

  • JenkinsRule
  • Add agent
  • Add credential (Must have valid private key for Jwt to work, but App doesn't have to actually exist)
  • Add wiremock responses for App, App Installation, and App Installation Token
  • Add log reader
  • Run pipeline that includes a withEnv that includes the credential
  • Verify correct messages from GitHubAppCredential logger indicating token was retrieved on agent

Not breaking this is pretty important. I'll add a test.

@bitwiseman bitwiseman requested review from jglick and dwnusbaum and removed request for jglick September 1, 2020 01:37
@bitwiseman bitwiseman force-pushed the issue/appid-expiration branch 2 times, most recently from 34e5409 to 399ee15 Compare September 1, 2020 08:15
@bitwiseman bitwiseman force-pushed the issue/appid-expiration branch from 399ee15 to 54e01d5 Compare September 1, 2020 08:21
Removed sorting that was likely to result in flaky results.
There is only one way the agent log makes sense and the output is still checked.
@bitwiseman bitwiseman force-pushed the issue/appid-expiration branch from 746d626 to 3ac6082 Compare September 1, 2020 08:42
Copy link
Member

@jglick jglick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certainly do not follow all the twists and turns of the token expiration logic, but overall it looks good, assuming sanity testing passes.

@@ -52,6 +52,10 @@
<artifactId>github</artifactId>
<version>1.31.0</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-support</artifactId>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Timeout I guess. Wonder if this should be moved to the likes of plugin-util-api

return appInstallationToken.getExpiresAt()
.toInstant()
.getEpochSecond();
} catch (Exception e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exception is expected here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused about this earlier too. IOException is declared to be thrown by GHAppInstallationToken.getExpiresAt, but looking at GitHubClient.parseDate I think the throws clause on GHAppInstallationToken.getExpiresAt could just be dropped. Date parsing could throw runtime exceptions like DateTimeParseException (not sure what would cause it in practice, perhaps changes on GitHub's side), so maybe that is the kind of thing this is trying to catch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation has changed over time, but changing the throws is an API change that will cause compilation to fail, right?

It is also possible for GitHub to hand us values that don't parse. Regardless of why it fails, we want to recover and continue.


private static final class DelegatingGitHubAppCredentials extends BaseStandardCredentials implements StandardUsernamePasswordCredentials {

private static final String SEP = "%%%";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to pick something unlikely to occur in any of the field values. I guess you could use JSONObject to be a little safer.

return appInstallationToken.getExpiresAt()
.toInstant()
.getEpochSecond();
} catch (Exception e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused about this earlier too. IOException is declared to be thrown by GHAppInstallationToken.getExpiresAt, but looking at GitHubClient.parseDate I think the throws clause on GHAppInstallationToken.getExpiresAt could just be dropped. Date parsing could throw runtime exceptions like DateTimeParseException (not sure what would cause it in practice, perhaps changes on GitHub's side), so maybe that is the kind of thing this is trying to catch.

@bitwiseman bitwiseman force-pushed the issue/appid-expiration branch from cefdf52 to d80588e Compare September 1, 2020 19:32
now = Instant.now().getEpochSecond();
token = new GitHubAppCredentials.AppInstallationToken("", now);
assertThat(token.isStale(), is(false));
assertThat(token.getTokenStaleEpochSeconds(), equalTo(now + GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observed to flake in #527:

java.lang.AssertionError: 

Expected: <1648072806L>
     but: was <1648072807L>
	at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
	at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:6)
	at org.jenkinsci.plugins.github_branch_source.GithubAppCredentialsAppInstallationTokenTest.testAppInstallationTokenStale(GithubAppCredentialsAppInstallationTokenTest.java:23)

I do not think you can make an assertion like this without some grace period.

jglick added a commit to jglick/github-branch-source-plugin-1 that referenced this pull request Mar 24, 2022
… flaking; use `closeTo` for actual times
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants