Skip to content

Commit

Permalink
feat: implement multiple auth strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
jkroepke committed Nov 1, 2022
1 parent ee67952 commit 7101509
Show file tree
Hide file tree
Showing 19 changed files with 1,360 additions and 538 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on:
pull_request:
push:
branches:
- master
- main
paths-ignore:
- 'README.md'

Expand All @@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '12.x'
node-version: '16.x'
- run: npm ci
- run: npm run lint
- run: npm run test
Expand Down
75 changes: 57 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,33 +50,36 @@ More information:
```
index.js -t <token> -i 600 [ -s ] [ -l :: ] [ -p 9171 ] [ -o organization ] [ -u user ] [ -r owner/repository ]
Authentication:
--auth-strategy GitHub auth strategy [required] [choices: "token", "oauth-app", "app", "action"] [default: "token"]
-a, --auth GitHub auth data (e.g.: token) [required]
Scape settings:
--interval, -i scrape interval [number] [default: 600]
--spread, -s spread request over interval [boolean] [default: false]
--scraper, -S enable or disable scraper [array] [default: ["summarize","extended-summarize","rate-limit","contributors","status","traffic-clones","traffic-top-paths","traffic-top-referrers","traffic-views"]]
-i, --interval scrape interval [number] [default: 600]
-s, --spread spread request over interval [boolean] [default: false]
-S, --scraper enable or disable scraper [array] [default: ["summarize","extended-summarize","rate-limit","contributors","status","traffic-clones","traffic-top-paths","traffic-top-referrers","traffic-views"]]
Scape targets:
--organization, -o GitHub organization to scrape. Can be defined multiple times or comma separated list [array] [default: []]
--user, -u GitHub users to scrape. Can be defined multiple times or comma separated list [array] [default: []]
--repository, -r GitHub repositories to scrape. Can be defined multiple times or comma separated list. Format: <owner>/<repo> [array] [default: []]
-o, --organization GitHub organization to scrape. Can be defined multiple times or comma separated list [array] [default: []]
-u, --user GitHub users to scrape. Can be defined multiple times or comma separated list [array] [default: []]
-r, --repository GitHub's repositories to scrape. Can be defined multiple times or comma-separated list. Format: <owner>/<repo> [array] [default: []]
Bind options:
--host address to bind exporter [default: "::"]
--port, -p port to bind exporter [number] [default: 9171]
--host address to bind exporter [default: "::"]
-p, --port port to bind exporter [number] [default: 9171]
Log options:
--log-level log level of application [choices: "error", "warn", "info", "http", "verbose", "debug", "silly"] [default: "info"]
--log-file path to log file
--log-console log to console [boolean] [default: true]
--log-format log format of application [default: "cli"]
--log-level log level of application [choices: "error", "warn", "info", "http", "verbose", "debug", "silly"] [default: "info"]
--log-file path to log file
--log-console log to console [boolean] [default: true]
--log-format log format of application [default: "cli"]
Options:
--version Show version number [boolean]
--config Path to JSON config file
--token, -t GitHub Personal access token [required]
--help, -h Show help [boolean]
--version Show version number [boolean]
--config Path to JSON config file
-h, --help Show help [boolean]
Environment variable support. Prefix: GITHUB_EXPORTER, e.g. --token == GITHUB_EXPORTER_TOKEN
Environment variable support. Prefix: GITHUB_EXPORTER, e.g. --auth == GITHUB_EXPORTER_AUTH
for more information, find our manual at https://github.com/jkroepke/github_exporter
```
Expand All @@ -88,14 +91,50 @@ them in a `.env` file.
More information about `.env` file:
- https://github.com/motdotla/dotenv#usage

## Authentication

`github_exporter` supports

* [token](https://github.com/octokit/auth-token.js)
* [oauth-app](https://github.com/octokit/auth-oauth-app.js)
* [app](https://github.com/octokit/auth-app.js)
* [action](https://github.com/octokit/auth-action.js)

authentication types.

### token

Just pass your personal token as an argument

```bash
github_exporter --auth ghp_xxx
```

### oauth-app

Pass all properties as JSON object

```bash
github_exporter --auth-strategy oauth-app --auth '{"clientType": "oauth-app", "clientId": "1234567890abcdef1234", "clientSecret": "1234567890abcdef1234567890abcdef12345678"}'
```

### app

Pass all properties as JSON object. Authenticate as in app is currently not supported

```bash
# Authenticate as OAuth App (client ID/client secret)
github_exporter --auth-strategy app --auth '{"appId": 1, "privateKey": "-----BEGIN PRIVATE KEY-----\n...", "clientId": "1234567890abcdef1234", "clientSecret": "1234567890abcdef1234567890abcdef12345678", "installationId": 123}'
```

## Start the exporter

### Docker:

```bash
docker run --name github_exporter -d \
--restart=always -p 9171:9171 \
-e GITHUB_EXPORTER_TOKEN=<secret> \
-e GITHUB_EXPORTER_AUTH=<secret> \
-e GITHUB_EXPORTER_ORGANIZATION=org1,org2 \
-e GITHUB_EXPORTER_USER=user1,user2 \
-e GITHUB_EXPORTER_REPOSITORY=jkroepke/github_exporter,jkroepke/helm-secrets,jkroepke/2Moons \
Expand Down
43 changes: 32 additions & 11 deletions lib/args.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,29 @@ const winston = require('winston')
const commaListToArray = (opt) => opt.map((element) => element.split(',')).flat()

module.exports.argv = require('yargs')
.usage('$0 -t <token> -i 600 [ -s ] [ -l :: ] [ -p 9171 ] [ -o organization ] [ -u user ] [ -r owner/repository ]')
.config('config', cfgPath => JSON.parse(fs.readFileSync(cfgPath, 'utf8')))
.option('token', {
alias: 't',
describe: 'GitHub Personal access token',
.usage(
'$0 -t <token> -i 600 [ -s ] [ -l :: ] [ -p 9171 ] [ -o organization ] [ -u user ] [ -r owner/repository ]'
)
.config('config', (cfgPath) => JSON.parse(fs.readFileSync(cfgPath, 'utf8')))

.option('auth-strategy', {
describe: 'GitHub auth strategy',
demandOption: true,
nargs: 1,
group: 'Authentication:',
default: 'token',
choices: ['token', 'oauth-app', 'app', 'action']
})
.option('auth', {
alias: 'a',
conflicts: 'token',
describe: 'GitHub auth data (e.g.: token)',
demandOption: true,
nargs: 1
group: 'Authentication:',
nargs: 1,
coerce: function (arg) {
return arg.startsWith('{') ? JSON.parse(arg) : arg
}
})
.option('interval', {
alias: 'i',
Expand Down Expand Up @@ -46,7 +62,8 @@ module.exports.argv = require('yargs')
})
.option('organization', {
alias: 'o',
describe: 'GitHub organization to scrape. Can be defined multiple times or comma separated list',
describe:
'GitHub organization to scrape. Can be defined multiple times or comma separated list',
group: 'Scape targets:',
array: true,
default: [],
Expand All @@ -62,7 +79,8 @@ module.exports.argv = require('yargs')
})
.option('repository', {
alias: 'r',
describe: 'GitHub repositories to scrape. Can be defined multiple times or comma separated list. Format: <owner>/<repo>',
describe:
"GitHub's repositories to scrape. Can be defined multiple times or comma-separated list. Format: <owner>/<repo>",
group: 'Scape targets:',
array: true,
default: [],
Expand Down Expand Up @@ -109,8 +127,11 @@ module.exports.argv = require('yargs')
}
})
.env('GITHUB_EXPORTER')
.showHelpOnFail(true)
.help('help')
.alias('help', 'h')
.epilogue('Environment variable support. Prefix: GITHUB_EXPORTER, e.g. --token == GITHUB_EXPORTER_TOKEN\n\nfor more information, find our manual at https://github.com/jkroepke/github_exporter')
.wrap(null)
.argv
.epilogue(
'Environment variable support. Prefix: GITHUB_EXPORTER, e.g. --auth == GITHUB_EXPORTER_AUTH\n\nfor more information, find our manual at https://github.com/jkroepke/github_exporter'
)
.strict()
.wrap(null).argv
24 changes: 22 additions & 2 deletions lib/github/graphql.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,28 @@ const defaults = {
}
}

if (argv.token) {
defaults.headers.authorization = `token ${argv.token}`
switch (argv.authStrategy) {
case 'action': {
const { createActionAuth } = require('@octokit/auth-action')
defaults.request.hook = createActionAuth(argv.auth)
break
}
case 'oauth-app': {
const { createOAuthAppAuth } = require('@octokit/auth-oauth-app')
defaults.request.hook = createOAuthAppAuth(argv.auth)
break
}
case 'app': {
const { createAppAuth } = require('@octokit/auth-app')
defaults.request.hook = createAppAuth(argv.auth)
break
}
case 'token': {
defaults.headers.authorization = `token ${argv.auth}`
break
}
default:
break
}

module.exports = graphql.defaults(defaults)
30 changes: 24 additions & 6 deletions lib/github/rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,35 @@ const { argv } = require('../args')
const logger = require('../logger')

const options = {
auth: argv.auth,
userAgent: `github_exporter/${version} (jkroepke/github_exporter; +https://github.com/jkroepke/github_exporter)`,
log: {
debug: (message, additionalInfo) => logger.debug(message, additionalInfo),
info: (message, additionalInfo) => logger.verbose(message, additionalInfo),
warn: (message, additionalInfo) => logger.warn(message, additionalInfo),
error: (message, additionalInfo) => logger.error(message, additionalInfo)
debug: (message) => logger.debug(message),
info: (message) => logger.verbose(message),
warn: (message) => logger.warn(message),
error: (message) => logger.error(message)
}
}

if (argv.token) {
options.auth = argv.token
switch (argv.authStrategy) {
case 'action': {
const { createActionAuth } = require('@octokit/auth-action')
options.authStrategy = createActionAuth
break
}
case 'oauth-app': {
const { createOAuthAppAuth } = require('@octokit/auth-oauth-app')
options.authStrategy = createOAuthAppAuth
break
}
case 'app': {
const { createAppAuth } = require('@octokit/auth-app')
options.authStrategy = createAppAuth
break
}
case 'token':
default:
break
}

const octokit = new Octokit(options)
Expand Down
28 changes: 16 additions & 12 deletions lib/helpers.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
// https://stackoverflow.com/a/37826698/8087167
const splitArray = (array, chunkSize) => array.reduce((all, one, i) => {
const ch = Math.floor(i / chunkSize)
const splitArray = (array, chunkSize) =>
array.reduce((all, one, i) => {
const ch = Math.floor(i / chunkSize)

// eslint-disable-next-line no-param-reassign
all[ch] = [].concat((all[ch] || []), one)
// eslint-disable-next-line no-param-reassign
all[ch] = [].concat(all[ch] || [], one)

return all
}, [])
return all
}, [])

const transformRepositoryNameToGraphQlLabel = (name) => name.replace(/[-/.]/g, '_')

const generateGraphqlRepositoryQueries = (repositories) => repositories.map((repository) => {
const [owner, name] = repository.split('/')
const label = transformRepositoryNameToGraphQlLabel(repository)
const generateGraphqlRepositoryQueries = (repositories) =>
repositories
.map((repository) => {
const [owner, name] = repository.split('/')
const label = transformRepositoryNameToGraphQlLabel(repository)

return `repo_${label}: repository(owner: "${owner}", name: "${name}") {\n...repositoryFragment\n}`
}).join('\n\n')
return `repo_${label}: repository(owner: "${owner}", name: "${name}") {\n...repositoryFragment\n}`
})
.join('\n\n')

const getWeekNumber = (date) => {
// https://stackoverflow.com/a/6117889/8087167
Expand All @@ -24,7 +28,7 @@ const getWeekNumber = (date) => {
d.setUTCDate(d.getUTCDate() + 4 - dayNum)
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))

return Math.ceil((((d - yearStart) / 86400000) + 1) / 7)
return Math.ceil(((d - yearStart) / 86400000 + 1) / 7)
}

module.exports = {
Expand Down
18 changes: 11 additions & 7 deletions lib/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@ const logger = winston.createLogger({
})

if (argv['log-console']) {
logger.add(new winston.transports.Console({
format: winston.format[argv['log-format']]()
}))
logger.add(
new winston.transports.Console({
format: winston.format[argv['log-format']]()
})
)
}

if (argv['log-file']) {
logger.add(new winston.transports.File({
filename: argv['log-file'],
format: winston.format[argv['log-format']]()
}))
logger.add(
new winston.transports.File({
filename: argv['log-file'],
format: winston.format[argv['log-format']]()
})
)
}

module.exports = logger
35 changes: 22 additions & 13 deletions lib/scraper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ const { argv } = require('./args')
const ghRestApi = require('./github/rest')

// eslint-disable-next-line global-require, import/no-dynamic-require
const scraperModules = argv.scraper.map((scraperModule) => new (require(`./scraper/${scraperModule}`))())
const globalScraperModules = scraperModules.filter((module) => typeof module.scrapeGlobal === 'function')
const repositoryScraperModules = scraperModules.filter((module) => typeof module.scrapeRepositories === 'function')
const scraperModules = argv.scraper.map(
(scraperModule) => new (require(`./scraper/${scraperModule}`))()
)
const globalScraperModules = scraperModules.filter(
(module) => typeof module.scrapeGlobal === 'function'
)
const repositoryScraperModules = scraperModules.filter(
(module) => typeof module.scrapeRepositories === 'function'
)

const scrapeRepositories = (repositories) => {
repositoryScraperModules.forEach((scraper) => {
Expand Down Expand Up @@ -36,12 +42,15 @@ const initScrapeOrganization = (organization, interval, spread) => {
org: organization
})

ghRestApi.paginate(
options,
(response) => response.data.map((repository) => repository.full_name)
)
ghRestApi
.paginate(options, (response) => response.data.map((repository) => repository.full_name))
.then((repositories) => scrapeRepositories(repositories))
.catch((err) => logger.error(`Failed to get all repository from organization ${organization} via REST: `, err.message))
.catch((err) =>
logger.error(
`Failed to get all repository from organization ${organization} via REST: `,
err.message
)
)
}

setTimeout(() => {
Expand All @@ -59,12 +68,12 @@ const initScrapeUser = (username, interval, spread) => {
type: 'owner'
})

ghRestApi.paginate(
options,
(response) => response.data.map((repository) => repository.full_name)
)
ghRestApi
.paginate(options, (response) => response.data.map((repository) => repository.full_name))
.then((repositories) => scrapeRepositories(repositories))
.catch((err) => logger.error(`Failed to get all repository from user ${username} via REST: `, err.message))
.catch((err) =>
logger.error(`Failed to get all repository from user ${username} via REST: `, err.message)
)
}

setTimeout(() => {
Expand Down
Loading

0 comments on commit 7101509

Please sign in to comment.