Initial implementation of github-nginx-cache (#1)

This commit is contained in:
billytrend 2019-07-12 11:58:09 -07:00 коммит произвёл GitHub
Родитель 83779dcac3
Коммит 67cc01d2b2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
34 изменённых файлов: 6579 добавлений и 331 удалений

26
.ci/merge-validation.yml Normal file
Просмотреть файл

@ -0,0 +1,26 @@
pool:
vmImage: "Ubuntu 16.04"
trigger:
- master
steps:
- task: Docker@2
displayName: Build docker container
inputs:
command: build
arguments: -t testbuild
- task: Docker@2
displayName: Run docker container
inputs:
command: run
arguments: -p 8000:80 -d testbuild
- script: npm -s ci
displayName: "Install dependencies"
workingDirectory: ./test
- script: npm run -s test
displayName: "Run tests"
workingDirectory: ./test

22
.ci/publish.yml Normal file
Просмотреть файл

@ -0,0 +1,22 @@
trigger:
- master
pr: none
name: v1.$(Date:yyyy-MM-dd)$(Rev:.r)
stages:
- stage: Build_Docker
jobs:
- job: Build_Docker
displayName: Build and publish to hub.docker.com
pool:
vmImage: "Ubuntu-16.04"
steps:
- task: Docker@2
displayName: "Build azuredevx/github-nginx-cache"
inputs:
containerRegistry: "github-nginx-cache docker"
repository: "azuredevx/github-nginx-cache"
tags: |
$(Build.BuildNumber)
latest

3
.dockerignore Normal file
Просмотреть файл

@ -0,0 +1,3 @@
*
!nginx-config
!kubernetes/active-passive

333
.gitignore поставляемый
Просмотреть файл

@ -1,330 +1,3 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
nginx-logs
**/node_modules
**/buildcache

11
.vscode/nginx-cache.code-workspace поставляемый Normal file
Просмотреть файл

@ -0,0 +1,11 @@
{
"folders": [
{
"path": "/Users/billytrend/Repositories/nginx-cache"
},
{
"path": "/Users/billytrend/Repositories/nginx-cache/test"
}
],
"settings": {}
}

9
Dockerfile Normal file
Просмотреть файл

@ -0,0 +1,9 @@
FROM nginx
RUN apt-get update; apt-get install -y curl
COPY ./nginx-config/ /etc/nginx/
COPY kubernetes/active-passive/check-readiness.sh /check-readiness.sh
# test nginx config
RUN nginx -c /etc/nginx/nginx.conf -t

Просмотреть файл

@ -1,7 +1,75 @@
## Github nginx cache
This repo contains nginx configuration tuned to sit in front of github endpoints and provide caching functionality. Github will not rate-limit [conditional requests](https://developer.github.com/v3/#conditional-requests). The [`proxy_cache_*` nginx directives](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache) force nginx to revalidate any cached content from the upstream server (in this case, github). Revalidation is performed by nginx as a conditional request, therefore it will not reduce api limits. This works for both authenticated and unauthenticated requests.
Here is an example how rate-limiting is mitigated for unauthenticated requests against both https://api.github.com and the cache running on http://localhost:8000.
![Rate limiting example](docs/rate-limit-example.png)
### Quick Start
docker run -d -p 8000:80 azure-devex/github-nginx-cache
curl localhost:8000/api/repos/azure/github-nginx-cache
The github domains are mapped as follows:
| Github URL | Cache URL |
| ---------------------------- | -------------------------- |
| api.github.com/\* | localhost:8000/api/\* |
| raw.githubusercontent.com/\* | localhost:8000/raw/\* |
| codeload.github.com/\* | localhost:8000/codeload/\* |
### CI/CD
Docker publish [![Build Status](https://dev.azure.com/azure-sdk/public/_apis/build/status/Azure.github-nginx-cache%20Publish?branchName=master)](https://dev.azure.com/azure-sdk/public/_build/latest?definitionId=496&branchName=master)
## Develop
### Build
docker build .
### Debug
#### Fish
docker build -t custom-nginx . && docker run -it -p 8000:80 -v (pwd)/nginx-logs:/var/log/nginx custom-nginx
curl localhost:8000/health/alive
#### Bash
docker build -t custom-nginx . && docker run -it -p 8000:80 -v $(pwd)/nginx-logs:/var/log/nginx custom-nginx
curl localhost:8000/health/alive
### Test
# Run image on localhost:8000
cd test
npm ci
npm run test
## Implementation details
### Github consistency
The cache is designed for the highest possible github consistency such that it ignores any `Cache-Control` headers that github sends and forces nginx to REVALIDATE for every request. A limitation in nginx means that the lowest value for `proxy_cache_valid` directive is one second. This means that two identical requests to github within the space of one second will HIT (return cached response without revalidating) rather than REVALIDATE.
### Cache partitioning
The cache may be used for complicated applications where multiple app and oauth tokens are being used to access github. The default behaviour in this case is to parition the cache by token. This means that a request with token A will not leverage any cached content from requests using token B.
This behaviour is for the following reasons.
1. Security - there are edge cases in which using two tokens within one second of each other could cause a response to be leaked to the second request even if the second token was not allowed to access the resource.
1. Prevent cache churn - if the cache was not partitioned, multiple requests to one api route with different tokens may cause the cache to be evacuated unnecessarily if these tokens have different access permissions.
There may be cases however where this behaviour needs to be overridden at the discretion of the client. For example when using a GitHub app, the token may expire every hour or so in which case the default behaviour would be for the cache to reset every hour which is not desirable.
By setting **X-Cache-Key** header, the cache [will be paritioned](nginx-config/cache_key_logic.conf) on this arbitrary string rather than the token.
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.

Двоичные данные
docs/rate-limit-example.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 72 KiB

Просмотреть файл

@ -0,0 +1,28 @@
#!/bin/bash
podhealth() {
if [[ -z "${podname}" ]]; then
echo "podname not set"
return 1
else
echo "podname is set to ${podname}"
fi
activepod=$(for i in 0 1; do echo $podname-$i;done | grep -v $HOSTNAME)
echo "activepod = ${activepod}.${serviceDomain}"
# ${activepod}.${serviceDomain} is the FQDN of a pod
# eg mypod.myservice.mynamespace.svc.cluster.local
curl -I ${activepod}.${serviceDomain}
# if curl fails then we need to be active otherwise stay passive
if [ $? -eq 0 ]
then
return 1
else
return 0
fi
}
podhealth

Просмотреть файл

@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: github-nginx-cache-loadbalancer
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: github-nginx-cache
type: LoadBalancer

Просмотреть файл

@ -0,0 +1,24 @@
apiVersion: v1
kind: Pod
metadata:
name: github-nginx-cache
labels:
app: github-nginx-cache
spec:
containers:
- image: azuredevx/github-nginx-cache:latest # specify the release for production use
name: mypod
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
volumeMounts:
- name: volume
mountPath: /mnt/github-nginx-cache
volumes:
- name: volume
persistentVolumeClaim:
claimName: managed-disk

Просмотреть файл

@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: managed-disk
spec:
accessModes:
- ReadWriteOnce
storageClassName: managed-premium
resources:
requests:
storage: 200Gi

Просмотреть файл

@ -0,0 +1,7 @@
set $cache_key $http_authorization;
# if x-cache-key header is present, use this to partition the nginx cache
# instead of the users token
if ($http_x_cache_key) {
set $cache_key $http_x_cache_key;
}

Просмотреть файл

@ -0,0 +1,23 @@
expires max;
# ignore the cache control headers that git sends so that we can revalidate more aggresively
proxy_ignore_headers Cache-Control;
# enable revalidation
proxy_cache_revalidate on;
# enable request coalescing so multiple requests are combined into one
proxy_cache_lock on;
proxy_cache_lock_timeout 2s;
# cache is valid for max 1s so we can be out of sync with github for max that amount of time
# nginx does not support a lower value for this
proxy_cache_valid 1s;
# cache storage config
proxy_cache cache_zone;
# use stale response if github is not available
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
# add x-cached header to responses to report how the cache was used
add_header X-Cached $upstream_cache_status;
# cache key
proxy_cache_key $cache_key:$request_to_proxy:$is_args:$args;
# rewrite the host header so github is not confused
proxy_set_header HOST $downstream_url;
# set target for proxying
proxy_pass https://$downstream_url$request_to_proxy$is_args$args;

Просмотреть файл

@ -0,0 +1,6 @@
location ~ /api(?<request_to_proxy>\/.*) {
set $downstream_url "api.github.com";
include cache_key_logic.conf;
include common_cache_settings.conf;
}

Просмотреть файл

@ -0,0 +1,6 @@
location ~ /codeload(?<request_to_proxy>\/.*) {
set $downstream_url "codeload.github.com";
include cache_key_logic.conf;
include common_cache_settings.conf;
}

Просмотреть файл

@ -0,0 +1,6 @@
location ~ /raw(?<request_to_proxy>\/.*) {
set $downstream_url "raw.githubusercontent.com";
include cache_key_logic.conf;
include common_cache_settings.conf;
}

Просмотреть файл

@ -0,0 +1,4 @@
location /health/alive {
return 200 '{ "message": "Github cache is alive" }';
add_header Content-Type application/json;
}

123
nginx-config/nginx.conf Normal file
Просмотреть файл

@ -0,0 +1,123 @@
user nginx;
# Set number of worker processes automatically based on number of CPU cores.
worker_processes auto;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
# Configures default error logger.
error_log /var/log/nginx/error.log warn;
# Includes files with directives to load dynamic modules.
include /etc/nginx/modules/*.conf;
events {
# The maximum number of simultaneous connections that can be opened by
# a worker process.
worker_connections 1024;
}
http {
# Includes mapping of file name extensions to MIME types of responses
# and defines the default type.
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Don't tell nginx version to clients.
server_tokens off;
# Specifies the maximum accepted body size of a client request, as
# indicated by the request header Content-Length. If the stated content
# length is greater than this size, then the client receives the HTTP
# error code 413. Set to 0 to disable.
client_max_body_size 1m;
# Timeout for keep-alive connections. Server will close connections after
# this time.
keepalive_timeout 65;
# Sendfile copies data between one FD and other from within the kernel,
# which is more efficient than read() + write().
sendfile on;
# Don't buffer data-sends (disable Nagle algorithm).
# Good for sending frequent small bursts of data in real time.
tcp_nodelay on;
# Specifies that our cipher suits should be preferred over client ciphers.
ssl_prefer_server_ciphers on;
# Enables a shared SSL cache with size that can hold around 8000 sessions.
ssl_session_cache shared:SSL:2m;
# Set the Vary HTTP header as defined in the RFC 2616.
gzip_vary on;
# Specifies the main log format
log_format access '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# Specifies the main log location
access_log /var/log/nginx/access.log access;
# logs proxy pass information
log_format upstreamlog '[$time_local] $remote_addr - $remote_user - $server_name to: $upstream_addr: $request upstream_response_time $upstream_response_time msec $msec request_time $request_time';
# Specifies the upstream log location
access_log /var/log/nginx/upstream.log upstreamlog;
# Specifies the cache_status log format.
log_format cache_status '[$time_local] "$request" $upstream_cache_status';
# Specifies the cache_status log location
access_log /var/log/nginx/cache_access.log cache_status;
# location and settings
proxy_cache_path /mnt/github-nginx-cache use_temp_path=off levels=1:2 keys_zone=cache_zone:200m max_size=200g inactive=1y;
log_format json_combined escape=json
'{'
'"time_local":"$time_local",'
'"remote_addr":"$remote_addr",'
'"request_method":"$request_method",'
'"request_uri":"$request_uri",'
'"status": "$status",'
'"body_bytes_sent":"$body_bytes_sent",'
'"request_time":"$request_time",'
'"http_referrer":"$http_referer",'
'"upstream_addr":"$upstream_addr",'
'"upstream_response_time":"$upstream_response_time",'
'"msec":"$msec",'
'"upstream_http_x_ratelimit_limit":"$upstream_http_x_ratelimit_limit",'
'"upstream_http_x_ratelimit_remaining":"$upstream_http_x_ratelimit_remaining",'
'"upstream_http_x_ratelimit_reset":"$upstream_http_x_ratelimit_reset",'
'"upstream_http_retry_after":"$upstream_http_retry_after",'
'"request_to_proxy":"$request_to_proxy$is_args$args",'
'"http_x_cache_key":"$http_x_cache_key",'
'"downstream_url":"$downstream_url",'
'"upstream_status":"$upstream_status",'
'"upstream_bytes_received":"$upstream_bytes_received",'
'"upstream_bytes_sent":"$upstream_bytes_sent",'
'"upstream_cache_status":"$upstream_cache_status",'
'"request_body":"$request_body",'
'"http_user_agent":"$http_user_agent"'
'}';
access_log /dev/stdout json_combined;
error_log /dev/stderr warn;
server {
resolver 8.8.8.8;
listen 80;
server_name _;
location / {
return 200 'This is a caching service for gitub: https://github.com/Azure/github-nginx-cache/';
add_header Content-Type text/plain;
}
include health_check.conf;
include github-domain-configs/*.conf;
}
}

2
test/.prettierrc.yml Normal file
Просмотреть файл

@ -0,0 +1,2 @@
trailingComma: "all"
printWidth: 120

0
test/jest-setup.ts Normal file
Просмотреть файл

22
test/jest.config.js Normal file
Просмотреть файл

@ -0,0 +1,22 @@
// @ts-check
/** @type {jest.InitialOptions} */
const config = {
transform: {
"^.+\\.ts$": "ts-jest",
},
moduleFileExtensions: ["ts", "js", "json", "node"],
moduleNameMapper: {},
collectCoverage: false,
globals: {
"ts-jest": {
tsConfig: "tsconfig.json",
},
},
setupFiles: ["<rootDir>/jest-setup.ts"],
testMatch: ["<rootDir>/src/**/*.test.ts"],
verbose: true,
testEnvironment: "node",
};
module.exports = config;

5804
test/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

45
test/package.json Normal file
Просмотреть файл

@ -0,0 +1,45 @@
{
"name": "github-nginx-cache-test",
"version": "0.1.0",
"description": "This project welcomes contributions and suggestions. Most contributions require you to agree to a\r Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\r the rights to use your contribution. For details, visit https://cla.microsoft.com.",
"scripts": {
"test": "jest",
"test:ci": "jest --ci --reporters=default --reporters=jest-junit",
"lint": "tslint -p tsconfig.json",
"test:watch": "jest --watch --coverage=false --config ./config/jest.dev.config.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Azure/github-nginx-cache.git"
},
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/Azure/github-nginx-cache/issues"
},
"homepage": "https://github.com/Azure/github-nginx-cache#readme",
"devDependencies": {
"@types/convict": "^4.2.1",
"@types/jest": "^24.0.13",
"@types/node-fetch": "^2.3.4",
"jest": "^24.8.0",
"jest-junit": "^6.4.0",
"prettier": "^1.17.1",
"ts-jest": "^24.0.2",
"tslint": "^5.16.0",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "^3.4.5"
},
"dependencies": {
"@octokit/rest": "^16.27.0",
"@types/node": "^12.0.2",
"convict": "^5.0.0",
"node-fetch": "^2.6.0",
"nodegit": "^0.24.3"
},
"jest-junit": {
"outputDirectory": ".",
"outputName": "test-results.xml"
}
}

102
test/src/api.test.ts Normal file
Просмотреть файл

@ -0,0 +1,102 @@
import Github from "@octokit/rest";
import { configuration } from "./config";
import { xCacheKey } from "./x-cache-key-plugin";
import { delay, getHeader } from "./utils";
Github.plugin(xCacheKey);
describe("API", () => {
const github = new Github({
baseUrl: configuration.localApiProxyUrl,
});
it("should go to github", async () => {
const a = await github.repos.get({
owner: "octocat",
repo: "Hello-World",
headers: { "x-cache-key": Math.random() },
} as any);
expect(getHeader(a, "x-cached")).toMatchInlineSnapshot(`"MISS"`);
});
it("should HIT for immediate re-request", async () => {
const key = Math.random();
const a = await github.repos.get({
owner: "octocat",
repo: "Hello-World",
headers: { "x-cache-key": key },
} as any);
expect(getHeader(a, "x-cached")).toMatchInlineSnapshot(`"MISS"`);
const b = await github.repos.get({
owner: "octocat",
repo: "Hello-World",
headers: { "x-cache-key": key },
} as any);
expect(getHeader(b, "x-cached")).toMatchInlineSnapshot(`"HIT"`);
});
it("should REVALIDATE after ~2 seconds", async () => {
const key = Math.random();
const a = await github.repos.get({
owner: "octocat",
repo: "Hello-World",
headers: { "x-cache-key": key },
} as any);
expect(getHeader(a, "x-cached")).toMatchInlineSnapshot(`"MISS"`);
await delay(2000);
const b = await github.repos.get({
owner: "octocat",
repo: "Hello-World",
headers: { "x-cache-key": key },
} as any);
expect(getHeader(b, "x-cached")).toMatchInlineSnapshot(`"REVALIDATED"`);
});
it("REVALIDATE should not affect API limit", async () => {
const key = Math.random();
const a = await github.repos.get({
owner: "octocat",
repo: "Hello-World",
headers: { "x-cache-key": key },
} as any);
await delay(2000);
const b = await github.repos.get({
owner: "octocat",
repo: "Hello-World",
headers: { "x-cache-key": key },
} as any);
expect(getHeader(a, "x-ratelimit-remaining")).toEqual(getHeader(b, "x-ratelimit-remaining"));
});
it("requests with different x-cache-key should have different caches", async () => {
const a = await github.repos.get({
owner: "octocat",
repo: "Hello-World",
headers: { "x-cache-key": Math.random() },
} as any);
expect(getHeader(a, "x-cached")).toMatchInlineSnapshot(`"MISS"`);
const b = await github.repos.get({
owner: "octocat",
repo: "Hello-World",
headers: { "x-cache-key": Math.random() },
} as any);
expect(getHeader(b, "x-cached")).toMatchInlineSnapshot(`"MISS"`);
});
});

Просмотреть файл

@ -0,0 +1,5 @@
import { configSchema } from "./schema";
configSchema.validate({ allowed: "strict" });
export const configuration = configSchema.getProperties();

1
test/src/config/index.ts Normal file
Просмотреть файл

@ -0,0 +1 @@
export * from "./configuration";

18
test/src/config/schema.ts Normal file
Просмотреть файл

@ -0,0 +1,18 @@
import convict from "convict";
export type Env = "test";
export const configSchema = convict({
env: {
doc: "The application environment.",
format: ["test"],
default: "test",
env: "NODE_ENV",
},
localApiProxyUrl: {
doc: "URL for caching api proxy",
format: "url",
default: "http://localhost:8000/api/", // *.vcap.me resolves to 127.0.0.1 so it can be used for testing subdomain requests to localhost
env: "localApiProxyUrl",
},
});

0
test/src/definitions.d.ts поставляемый Normal file
Просмотреть файл

14
test/src/utils.ts Normal file
Просмотреть файл

@ -0,0 +1,14 @@
import Github from "@octokit/rest";
export interface NginxHeaders {
"x-cached": "REVALIDATED" | "MISS" | "HIT";
}
export const getHeader = (
response: Github.Response<unknown>,
header: keyof (NginxHeaders & Github.Response<any>["headers"]),
) => {
return ((response.headers as unknown) as NginxHeaders & Github.Response<any>["headers"])[header];
};
export const delay = (ms: number) => new Promise(res => setTimeout(res, ms));

Просмотреть файл

@ -0,0 +1,63 @@
import Github from "@octokit/rest";
import { configuration } from "./config";
import { getHeader } from "./utils";
import { xCacheKey } from "./x-cache-key-plugin";
describe("xCacheKey", () => {
it("requests with different x-cache-key should have different caches", async () => {
const githubA = new (Github.plugin([xCacheKey]))({
baseUrl: configuration.localApiProxyUrl,
cacheKey: "a",
});
const a = await githubA.repos.get({
owner: "octocat",
repo: "Hello-World",
request: { cacheKey: Math.random() },
} as any);
expect(getHeader(a, "x-cached")).toMatchInlineSnapshot(`"MISS"`);
const githubB = new Github({
baseUrl: configuration.localApiProxyUrl,
cacheKey: "a",
});
const b = await githubB.repos.get({
owner: "octocat",
repo: "Hello-World",
headers: { "x-cache-key": Math.random() },
} as any);
expect(getHeader(b, "x-cached")).toMatchInlineSnapshot(`"MISS"`);
});
it("requests with same x-cache-key should have different caches", async () => {
const githubA = new Github({
baseUrl: configuration.localApiProxyUrl,
cacheKey: "a",
});
const a = await githubA.repos.get({
owner: "octocat",
repo: "Hello-World",
headers: { "x-cache-key": Math.random() },
} as any);
expect(getHeader(a, "x-cached")).toMatchInlineSnapshot(`"MISS"`);
const githubB = new Github({
baseUrl: configuration.localApiProxyUrl,
cacheKey: "a",
});
const b = await githubB.repos.get({
owner: "octocat",
repo: "Hello-World",
headers: { "x-cache-key": Math.random() },
} as any);
expect(getHeader(b, "x-cached")).toMatchInlineSnapshot(`"MISS"`);
});
});

Просмотреть файл

@ -0,0 +1,18 @@
import Octokit from "@octokit/rest";
export const xCacheKey: Octokit.Plugin = (octokit, options: { cacheKey?: string } & Octokit.Options) => {
octokit.hook.wrap("request", async (request, requestOptions) => {
const cacheKey: string | undefined = options.cacheKey || (requestOptions.request as any).cacheKey;
const augmentedOptions = cacheKey
? {
...requestOptions,
headers: { ...requestOptions.headers, "x-cache-key": cacheKey },
}
: requestOptions;
const response = await request(augmentedOptions);
return response;
});
};

49
test/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,49 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"rootDir": "src",
"outDir": "bin",
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "incremental": true, /* Enable incremental compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
"incremental": true,
"tsBuildInfoFile": "./buildcache/backend.tsbuildinfo",
/* Additional Checks */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
/* Source Map Options */
"sourceMap": true,
/* Experimental Options */
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */
},
"include": ["src/**/*.ts", "src/definitions.d.ts"],
"exclude": ["node_modules"]
}

43
test/tslint.json Normal file
Просмотреть файл

@ -0,0 +1,43 @@
{
"defaultSeverity": "error",
"extends": ["tslint:latest", "tslint-config-prettier", "tslint-plugin-prettier"],
"jsRules": {},
"rules": {
"file-name-casing": [true, "kebab-case"],
"no-unused-expression": [true, "allow-fast-null-checks"],
"interface-name": false,
"object-literal-sort-keys": false,
"no-object-literal-type-assertion": false,
"no-console": true,
"member-ordering": false,
"no-implicit-dependencies": [true, "dev"],
"prefer-conditional-expression": false,
"no-submodule-imports": false,
"no-default-export": true,
"prettier": [true, ".prettierrc.yml"],
"no-empty-interface": false,
"no-floating-promises": true, // No unawaited or caught promises(This could cause process exit in later version of node)
"ordered-imports": [
true,
{
"import-sources-order": "case-insensitive",
"named-imports-order": "lowercase-last",
"grouped-imports": true,
"groups": [
{ "match": "^\\..*$", "order": 20 }, // Have relative imports
{ "match": ".*", "order": 10 }
]
}
],
"ban": [
true,
{ "name": "fdescribe", "message": "Don't leave focus tests" },
{ "name": "fit", "message": "Don't leave focus tests" },
{ "name": "ftest", "message": "Don't leave focus tests" },
{ "name": ["describe", "only"], "message": "Don't leave focus tests" },
{ "name": ["it", "only"], "message": "Don't leave focus tests" },
{ "name": ["test", "only"], "message": "Don't leave focus tests" }
]
},
"rulesDirectory": []
}