-
-
Notifications
You must be signed in to change notification settings - Fork 23.6k
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
feat: add PAT monitoring functions #2178
Merged
Merged
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
a505538
feat: add PAT monitoring functions
rickstaa c239673
feat: add shields.io dynamic badge json response
rickstaa 26bc4bc
feat: add 'json' type to up monitor
rickstaa c261bdb
feat: cleanup status functions
rickstaa cf6e7c5
ci: decrease pat-info rate limiting time
rickstaa 85b4349
feat: decrease monitoring functions rate limits
rickstaa fbf2793
refactor: pat code
anuraghazra cd9e685
feat: add PAT monitoring functions
rickstaa a44cd16
feat: add shields.io dynamic badge json response
rickstaa 964ad66
feat: add 'json' type to up monitor
rickstaa 3c0aae1
feat: cleanup status functions
rickstaa c502d55
ci: decrease pat-info rate limiting time
rickstaa 3303f90
feat: decrease monitoring functions rate limits
rickstaa 106ce11
refactor: pat code
anuraghazra b866378
Merge branch 'monitoring' of https://github.com/anuraghazra/github-re…
anuraghazra 169fa96
test: fix pat-info tests
rickstaa 6015466
Update api/status/pat-info.js
rickstaa 8c4feef
test: fix broken tests
rickstaa b2381ee
Merge branch 'monitoring' of https://github.com/anuraghazra/github-re…
anuraghazra d3d1d1a
chore: fix suspended account
anuraghazra 2f14ed0
chore: simplify and refactor
anuraghazra bcde859
chore: fix test
anuraghazra b79cc44
chore: add resetIn field
anuraghazra File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/** | ||
* @file Contains a simple cloud function that can be used to check which PATs are no | ||
* longer working. It returns a list of valid PATs, expired PATs and PATs with errors. | ||
* | ||
* @description This function is currently rate limited to 1 request per day. | ||
*/ | ||
|
||
import { logger, request } from "../../src/common/utils.js"; | ||
|
||
export const RATE_LIMIT_SECONDS = 60 * 60 * 24; // 1 request per day. | ||
|
||
/** | ||
* Simple uptime check fetcher for the PATs. | ||
* | ||
* @param {import('axios').AxiosRequestHeaders} variables | ||
* @param {string} token | ||
*/ | ||
const uptimeFetcher = (variables, token) => { | ||
return request( | ||
{ | ||
query: ` | ||
query { | ||
rateLimit { | ||
remaining | ||
} | ||
} | ||
`, | ||
variables, | ||
}, | ||
{ | ||
Authorization: `bearer ${token}`, | ||
}, | ||
); | ||
}; | ||
|
||
/** | ||
* Check whether any of the PATs is expired. | ||
*/ | ||
const getPATInfo = async (fetcher, variables) => { | ||
let PATsInfo = { expiredPATs: [], errorPATs: [], validPATs: [] }; | ||
const PATs = Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key)); | ||
for (const pat of PATs) { | ||
try { | ||
const response = await fetcher(variables, process.env[pat]); | ||
response.data.errors && response.data.errors[0].type === "RATE_LIMITED"; | ||
|
||
// Store PATs with errors. | ||
if ( | ||
response.data.errors && | ||
!(response.data.errors[0].type === "RATE_LIMITED") | ||
) { | ||
PATsInfo.errorPATs.push({ | ||
[pat]: { | ||
type: response.data.errors[0].type, | ||
message: response.data.errors[0].message, | ||
}, | ||
}); | ||
continue; | ||
} | ||
|
||
// Store remaining PATs. | ||
PATsInfo.validPATs.push(pat); | ||
} catch (err) { | ||
const isBadCredential = | ||
err.response && | ||
err.response.data && | ||
err.response.data.message === "Bad credentials"; | ||
|
||
// Store the PAT if it is expired. | ||
if (isBadCredential) { | ||
PATsInfo.expiredPATs.push(pat); | ||
} else { | ||
throw err; | ||
} | ||
} | ||
} | ||
return PATsInfo; | ||
}; | ||
|
||
/** | ||
* Cloud function that returns information about the used PATs. | ||
*/ | ||
export default async (_, res) => { | ||
res.setHeader("Content-Type", "application/json"); | ||
try { | ||
// Add header to prevent abuse. | ||
const pATsInfo = await getPATInfo(uptimeFetcher, {}); | ||
rickstaa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (pATsInfo) { | ||
res.setHeader( | ||
"Cache-Control", | ||
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`, | ||
); | ||
} | ||
res.send(pATsInfo); | ||
} catch (err) { | ||
// Throw error if something went wrong. | ||
logger.error(err); | ||
res.setHeader("Cache-Control", "no-store"); | ||
res.send("Something went wrong: " + err.message); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/** | ||
* @file Contains a simple cloud function that can be used to check if the PATs are still | ||
* functional. | ||
* | ||
* @description This function is currently rate limited to 1 request per 15 minutes. | ||
*/ | ||
|
||
import { logger, request } from "../../src/common/utils.js"; | ||
|
||
// Script variables. | ||
const PATs = Object.keys(process.env).filter((key) => | ||
/PAT_\d*$/.exec(key), | ||
).length; | ||
const RETRIES = PATs ? PATs : 7; | ||
export const RATE_LIMIT_SECONDS = 60 * 15; // 1 request per 15 minutes | ||
|
||
/** | ||
* Simple uptime check fetcher for the PATs. | ||
* | ||
* @param {import('axios').AxiosRequestHeaders} variables | ||
* @param {string} token | ||
*/ | ||
const uptimeFetcher = (variables, token) => { | ||
return request( | ||
{ | ||
query: ` | ||
query { | ||
rateLimit { | ||
remaining | ||
} | ||
} | ||
`, | ||
variables, | ||
}, | ||
{ | ||
Authorization: `bearer ${token}`, | ||
}, | ||
); | ||
}; | ||
|
||
/** | ||
* Check whether any of the PATs is still functional. | ||
* | ||
* @param {Object} fetcher Fetcher object. | ||
* @param {Object} variables Fetcher variables. | ||
* @param {number} retries How many times to retry. | ||
*/ | ||
const PATsWorking = async (fetcher, variables, retries = 0) => { | ||
if (retries > RETRIES) { | ||
// Return false if no PAT is working. | ||
return false; | ||
} | ||
|
||
// Loop through PATs to see if any of them are working. | ||
try { | ||
const response = await fetcher( | ||
variables, | ||
process.env[`PAT_${retries + 1}`], | ||
); | ||
|
||
const isRateExceeded = | ||
response.data.errors && response.data.errors[0].type === "RATE_LIMITED"; | ||
|
||
// If rate limit is hit increase RETRIES and recursively call the PATsWorking | ||
// with username, and current RETRIES | ||
if (isRateExceeded) { | ||
logger.log(`PAT_${retries + 1} Failed`); | ||
retries++; | ||
return PATsWorking(fetcher, variables, retries); | ||
} | ||
|
||
return true; // Return true if a PAT was working. | ||
} catch (err) { | ||
// also checking for bad credentials if any tokens gets invalidated | ||
const isBadCredential = | ||
err.response && | ||
err.response.data && | ||
err.response.data.message === "Bad credentials"; | ||
|
||
if (isBadCredential) { | ||
logger.log(`PAT_${retries + 1} Failed`); | ||
retries++; | ||
// directly return from the function | ||
return PATsWorking(fetcher, variables, retries); | ||
} else { | ||
throw err; | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* Cloud function that returns whether the PATs are still functional. | ||
*/ | ||
export default async (_, res) => { | ||
res.setHeader("Content-Type", "application/json"); | ||
try { | ||
// Add header to prevent abuse. | ||
const PATsValid = await PATsWorking(uptimeFetcher, {}); | ||
if (PATsValid) { | ||
res.setHeader( | ||
"Cache-Control", | ||
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`, | ||
); | ||
} | ||
res.send(PATsValid); | ||
} catch (err) { | ||
// Return fail boolean if something went wrong. | ||
logger.error(err); | ||
res.setHeader("Cache-Control", "no-store"); | ||
res.send("Something went wrong: " + err.message); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
/** | ||
* @file Tests for the status/pat-info cloud function. | ||
*/ | ||
import dotenv from "dotenv"; | ||
dotenv.config(); | ||
|
||
import { jest } from "@jest/globals"; | ||
import axios from "axios"; | ||
import MockAdapter from "axios-mock-adapter"; | ||
import patInfo, { RATE_LIMIT_SECONDS } from "../api/status/pat-info.js"; | ||
|
||
const mock = new MockAdapter(axios); | ||
|
||
const successData = { | ||
rateLimit: { | ||
remaining: 4986, | ||
}, | ||
}; | ||
|
||
const faker = (query) => { | ||
const req = { | ||
query: { ...query }, | ||
}; | ||
const res = { | ||
setHeader: jest.fn(), | ||
send: jest.fn(), | ||
}; | ||
|
||
return { req, res }; | ||
}; | ||
|
||
const rate_limit_error = { | ||
errors: [ | ||
{ | ||
type: "RATE_LIMITED", | ||
}, | ||
], | ||
}; | ||
|
||
const other_error = { | ||
errors: [ | ||
{ | ||
type: "SOME_ERROR", | ||
message: "This is a error", | ||
}, | ||
], | ||
}; | ||
|
||
const bad_credentials_error = { | ||
message: "Bad credentials", | ||
}; | ||
|
||
afterEach(() => { | ||
mock.reset(); | ||
}); | ||
|
||
describe("Test /api/status/pat-info", () => { | ||
beforeAll(() => { | ||
process.env.PAT_1 = "testPAT1"; | ||
process.env.PAT_2 = "testPAT2"; | ||
process.env.PAT_3 = "testPAT3"; | ||
process.env.PAT_4 = "testPAT4"; | ||
}); | ||
|
||
it("should return only 'validPATs' if all PATs are valid", async () => { | ||
mock | ||
.onPost("https://api.github.com/graphql") | ||
.replyOnce(200, rate_limit_error) | ||
.onPost("https://api.github.com/graphql") | ||
.reply(200, successData); | ||
|
||
const { req, res } = faker({}, {}); | ||
await patInfo(req, res); | ||
|
||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); | ||
expect(res.send).toBeCalledWith({ | ||
errorPATs: [], | ||
expiredPATs: [], | ||
validPATs: ["PAT_1", "PAT_2", "PAT_3", "PAT_4"], | ||
}); | ||
}); | ||
|
||
it("should return `errorPATs` if a PAT causes an error to be thrown", async () => { | ||
mock | ||
.onPost("https://api.github.com/graphql") | ||
.replyOnce(200, other_error) | ||
.onPost("https://api.github.com/graphql") | ||
.reply(200, successData); | ||
|
||
const { req, res } = faker({}, {}); | ||
await patInfo(req, res); | ||
|
||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); | ||
expect(res.send).toBeCalledWith({ | ||
errorPATs: [ | ||
{ PAT_1: { type: "SOME_ERROR", message: "This is a error" } }, | ||
], | ||
expiredPATs: [], | ||
validPATs: ["PAT_2", "PAT_3", "PAT_4"], | ||
}); | ||
}); | ||
|
||
it("should return `expiredPaths` if a PAT returns a 'Bad credentials' error", async () => { | ||
mock | ||
.onPost("https://api.github.com/graphql") | ||
.replyOnce(404, bad_credentials_error) | ||
.onPost("https://api.github.com/graphql") | ||
.reply(200, successData); | ||
|
||
const { req, res } = faker({}, {}); | ||
await patInfo(req, res); | ||
|
||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); | ||
expect(res.send).toBeCalledWith({ | ||
errorPATs: [], | ||
expiredPATs: ["PAT_1"], | ||
validPATs: ["PAT_2", "PAT_3", "PAT_4"], | ||
}); | ||
}); | ||
|
||
it("should throw an error if something goes wrong", async () => { | ||
mock.onPost("https://api.github.com/graphql").networkError(); | ||
|
||
const { req, res } = faker({}, {}); | ||
await patInfo(req, res); | ||
|
||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); | ||
expect(res.send).toBeCalledWith("Something went wrong: Network Error"); | ||
}); | ||
|
||
it("should have proper cache when no error is thrown", async () => { | ||
mock.onPost("https://api.github.com/graphql").reply(200, successData); | ||
|
||
const { req, res } = faker({}, {}); | ||
await patInfo(req, res); | ||
|
||
expect(res.setHeader.mock.calls).toEqual([ | ||
["Content-Type", "application/json"], | ||
["Cache-Control", `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`], | ||
]); | ||
}); | ||
|
||
it("should have proper cache when error is thrown", async () => { | ||
mock.onPost("https://api.github.com/graphql").networkError(); | ||
|
||
const { req, res } = faker({}, {}); | ||
await patInfo(req, res); | ||
|
||
expect(res.setHeader.mock.calls).toEqual([ | ||
["Content-Type", "application/json"], | ||
["Cache-Control", "no-store"], | ||
]); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
We can change this limit to our likings.