{"action":"created","comment":{"url":"https://api.github.com/repos/dcos/adminrouter/pulls/comments/95794098","pull_request_review_id":16351396,"id":95794098,"diff_hunk":"@@ -1,10 +1,260 @@\n-# adminrouter\n-The admin router is an open-source Nginx configuration created by Mesosphere that provides central authentication and proxy to DCOS services within the cluster.\n+# Admin Router\n \n+The Admin Router is an open-source Nginx configuration created by\n+Mesosphere that provides central authentication and proxy to DC/OS services\n+within the cluster.\n \n <img src=\"admin-router.png\" alt=\"\" width=\"100%\" align=\"middle\">\n \n-## Ports\n+## Ports summary\n <img src=\"admin-router-table.png\" alt=\"\" width=\"100%\" align=\"middle\">\n- \n- \n+\n+## Testing\n+\n+Admin Router repository includes a test harness that is meant to make\n+testing easier and in some cases - possible. It's written in Python and\n+uses pytest fixtures and custom modules to mock out all relevant DC/OS\n+features and control Nginx startup and termination.\n+\n+All the tests are executed in a Docker container which is controlled by the\n+Makefile. Inside the container pytest command is started which in turn pulls\n+in all the relevant fixtures, such as syslog mock, mocker (DC/OS endpoints\n+mock), dns mock, etc... Finally, an Nginx is spawned using the configuration\n+bind-mounted from the developer's repository. Tests may launch Nginx multiple\n+times, in different configuration, depending on what is needed. After the\n+tests runner finishes, all the processes and the environment is cleaned up\n+by pytest.\n+\n+Below, there is a general overview of all the components of the test harness.\n+More detailed documentation can be found in docstrings and comments in the code\n+itself. If in doubt, it also may be very helpful to look for examples among\n+existing tests.\n+\n+### Quickstart\n+In order to execute all the test, just issue:\n+\n+\tmake test\n+\n+In order have fine grained control over pytest command, execute:\n+\n+\tmake shell\n+\n+This will launch an interactive environment inside the container that has all\n+the dependencies installed. The developer may now launch the `pytest` command\n+the way it's necessary, debug the environment and temporarily add/change\n+the dependencies.\n+\n+### Makefile\n+Makefile provides an easy way to start the testing environment without the need\n+to worry about the correct docker commands. It's core concept is `adminrouter-devkit`\n+container which is contains all the dependencies that are needed to run\n+Admin Router, and inside which all tests-related commands are run.\n+\n+It exposes a couple of targets:\n+* `make clean` - remove all containers created by the test harness. It does not\n+  remove the images themselves though as the layer cache may be useful later\n+  on and the user may remove them themselves.\n+* `make devkit` - creates the `adminrouter-devkit` container. By default other\n+   targets execute this step automatically if devkit container image does not\n+   exist yet.\n+* `make update-devkit` - updates `adminrouter-devkit`. Should be run every time\n+   the Dockerfile or its dependencies change.\n+* `make tests` - launch all the tests. Worth noting is the fact that mccabe\n+  complexity of the code is also verified and an error is raised if it's equal\n+  or above 10.\n+* `make shell` - launch an interactive shell within the devkit container. Should\n+  be used in case when fine grained control of the tests is necessary or\n+  during debugging.\n+* `make flake8` - launch flake8 which will check all the tests and test-harness\n+  files by default.\n+\n+### Docker container\n+As mentioned earlier, all the commands are executed inside the `adminrouter-devkit`\n+container. It follows the same build process for Nginx that happens during\n+DC/OS build with the execption of setting the  `--with-debug` flag. It also\n+contains some basic debugging tools, pytest related dependencies and basic\n+files that help pytest mimic the DC/OS environment. Their location is then\n+exported via environment variables to the pytest code, so changing their location\n+can be done by edition only the Dockerfile.\n+\n+Due to some issues with overlayfs on older kernels, the container links\n+/tmp directory to /dev/shm.\n+\n+### Repository flavours\n+Admin Router repository can come in two variants/flavours - Enterprise and Opensource.\n+Tests determine it basing on the directory structure - if tests contain\n+`test-harness/tests/open/` directory then the repo is treated as Opensource one,\n+and in case when `test-harness/tests/ee/` directory is present - as the\n+enterprise one. Pytest fixture `repo_is_ee` takes care of it and is pulled in\n+as dependencies by all the fixtures that need this information.\n+\n+### Service startup ordering and cleanup\n+Mocking out DC/OS is a complex task, and a huge part in it have pytest\n+fixtures. They take care of proper ordering of mocks and subprocess start/stop and\n+cleanup and the cleanup ordering itself.\n+\n+Tracking the chain of fixtures and how they use each other may provide better\n+understanding of how the test harness works.\n+\n+### JWT\n+DC/OS IAM relies on JSON Web Tokens for transmitting security information between DC/OS\n+components. Open repository flavour uses HS256 tokens, while EE relies on RS256.\n+Test harness needs to have a way to create them for use in the tests. This is\n+done by `test-harness/modules/mocker/jwt.py` module, which together with\n+`repo_is_ee` fixture provides abstracts away the type of the\n+token used by Admin Router itself.\n+\n+### Mocker\n+Mocker takes care of simulating DC/OS HTTP endpoints that Admin Router uses. It's\n+basically just a thin management layer on top of multiple python-based\n+HTTP servers, each one of them mocking out particular component/upstream of DC/OS.\n+\n+It exposes the `.send_command()` method that allows to reconfigure endpoints\n+the way the tests need it. It basically calls a getattr on the endpoint\n+instance in order to execute given function and pass it attributes as specified\n+by the `.send_command()` call arguments. Please see the function signature in\n+the file `test-harness/modules/mocker/common.py` for details.\n+\n+#### Endpoints\n+Endpoints are threading HTTP servers based on http.server python module. Because\n+of the fact that there are many different kinds of endpoints that need to be\n+employed by mocker, and that there is a lot of shared behaviour between them,\n+they follow inheritance tree as below:\n+\n+<img src=\"endpoint_class_hierarchy.png\" alt=\"\" width=\"100%\" align=\"middle\">\n+\n+* `ReflectingUnixSocketEndpoint`, `ReflectingTCPIPEndpoint`: both of them send back\n+  the request data in the response body to the client for inspection. The only\n+  difference between them is that the former is listening on Unix Socket\n+  and the latter is listening on TCP/IP socket. They are useful for very simple\n+  tests that only check if the request is hitting the right upstream and the\n+  headers are correct.\n+* `IAMEndpoint`, `MarathonEndpoint`, `MesosEndpoint`: specialized endpoints that\n+  are mimicking IAM, Marathon and Mesos respectively. Apart from the basic\n+  functionality (reset, bork, etc...), they are also capable of recording requests\n+  sent to them, and then returning them through mockers `send_command` back to the\n+  tests code.\n+* all remaining endpoints depicted in the hierarchy are not directly usable but\n+  can be inherited from and extended if necessary in order to meet the\n+  requirements of testing code.\n+\n+Each endpoint has an id, that it basically it's http address (i.e.\n+`http://127.0.0.1:8080` or `http:///run/dcos/dcos-metrics-agent.sock`). It's\n+available via the `.id()` method. They are started and stopped via `.start()`\n+and `.stop()` methods during the start and stop of mocker instance\n+respectively. Each endpoint can be set to respond to each and every request\n+with an error (`500 Internal server error`).\n+\n+#### Subprocess management\n+Pytest fixtures start a couple of subprocesses:\n+* two dnsmasq instances\n+* Admin Router itself\n+\n+These do not always log to stderr/stdout, so a very simple syslog mock is also\n+provided. All the stdouts and stderrs are piped into the central log\n+processing class LogCatcher. \n+\n+##### LogCatcher\n+As mentioned in the previous paragraph, all the stdout/stderr file descriptors\n+and syslog messages are piped into this class. It uses `poll()` call to monitor\n+all the sources for new information and push it into:\n+* standard python logging module\n+* logs located in `test-harness/logs/`\n+* internal buffer which can be used by tests for logging-based testing\n+\n+The internal buffer is available through\n+`stdout_line_buffer`/`stderr_line_buffer` methods of AR object, Syslog\n+object(available through a fixture), and dnsmasq processes (also through\n+fixture). The buffer itself is implemented as a plain python list where each\n+log line represents a single entry. This list is shared across all the objects\n+that are groking the buffer, and there is no extra protection from manipulating\n+it from within tests so extra care needs to be taken.\n+\n+In order to simplify handling of the log lines buffers, `LineBufferFilter` class\n+has been created. It exposes two interfaces:\n+* context manager that allows for groking the buffer entries that were created\n+  while executing the context:\n+\n+  ```\n+    filter_string = 'validate_jwt_or_exit(): User not found: `{}`'.format(uid)\n+    lbf = LineBufferFilter(filter_string,\n+                            line_buffer=master_ar_process.stderr_line_buffer)\n+    with lbf:\n+        resp = requests.get(url,\n+                            allow_redirects=False,\n+                            headers=header)\n+    assert lbf.log_line_found\n+\n+  ```\n+* `scan_log_buffer()` method that scans all the entries, since the subprocess\n+  start.\n+  ```\n+    filter_string = 'Secret key not set or empty string.'\n+\n+    lbf = LineBufferFilter(filter_string,\n+                            line_buffer=ar_process_without_secret_key.stderr_line_buffer)\n+\n+    lbf.scan_log_buffer()\n+\n+    assert lbf.log_line_found is True\n+\n+  ```\n+Separation of the log entries stemming from different instances of given \n+Subprocess class in the logfile is done by placing following line in the log\n+file:\n+```\n+✄✄✄✄✄✄✄✄✄✄ Logging of this instance ends here ✄✄✄✄✄✄✄✄✄✄\n+\n+```\n+\n+##### DNS mock\n+It's easier to launch dnsmasq process that will serve a static entries from a\n+`/etc/hosts.dnsmasq` file than to write a fully conforming dns server in python.\n+All the entries in this file point to localhost where appropriately configured\n+endpoints are listening for connections, even though in real DC/OS instance\n+they would point to a different server/IP address.\n+\n+Dnsmasq processes log all the requests to stdout which in turn is pushed into\n+LogCatcher instance.\n+\n+##### Syslog mock\n+Syslog mock is a very simple Python hack - a DGRAM Unix Socket is created and\n+added to LogWatcher. LogWatcher itself takes care of draining data from it,\n+with the line length limit hard-coded to 4096 bytes.\n+\n+##### Nginx\n+Nginx subprocess is different from others in regard to its lifetime. Pytest","path":"README.md","position":231,"original_position":231,"commit_id":"01d293d0f49acfbbb3d25141034aef79c0952b18","original_commit_id":"01d293d0f49acfbbb3d25141034aef79c0952b18","user":{"login":"gpaul","id":442,"avatar_url":"https://avatars.githubusercontent.com/u/442?v=3","gravatar_id":"","url":"https://api.github.com/users/gpaul","html_url":"https://github.com/gpaul","followers_url":"https://api.github.com/users/gpaul/followers","following_url":"https://api.github.com/users/gpaul/following{/other_user}","gists_url":"https://api.github.com/users/gpaul/gists{/gist_id}","starred_url":"https://api.github.com/users/gpaul/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gpaul/subscriptions","organizations_url":"https://api.github.com/users/gpaul/orgs","repos_url":"https://api.github.com/users/gpaul/repos","events_url":"https://api.github.com/users/gpaul/events{/privacy}","received_events_url":"https://api.github.com/users/gpaul/received_events","type":"User","site_admin":false},"body":"The pytest fixture","created_at":"2017-01-12T13:51:24Z","updated_at":"2017-01-12T13:57:36Z","html_url":"https://github.com/dcos/adminrouter/pull/34#discussion_r95794098","pull_request_url":"https://api.github.com/repos/dcos/adminrouter/pulls/34","_links":{"self":{"href":"https://api.github.com/repos/dcos/adminrouter/pulls/comments/95794098"},"html":{"href":"https://github.com/dcos/adminrouter/pull/34#discussion_r95794098"},"pull_request":{"href":"https://api.github.com/repos/dcos/adminrouter/pulls/34"}}},"pull_request":{"url":"https://api.github.com/repos/dcos/adminrouter/pulls/34","id":98493596,"html_url":"https://github.com/dcos/adminrouter/pull/34","diff_url":"https://github.com/dcos/adminrouter/pull/34.diff","patch_url":"https://github.com/dcos/adminrouter/pull/34.patch","issue_url":"https://api.github.com/repos/dcos/adminrouter/issues/34","number":34,"state":"open","locked":false,"title":"Add test harness for Adminrouter","user":{"login":"vespian","id":2124609,"avatar_url":"https://avatars.githubusercontent.com/u/2124609?v=3","gravatar_id":"","url":"https://api.github.com/users/vespian","html_url":"https://github.com/vespian","followers_url":"https://api.github.com/users/vespian/followers","following_url":"https://api.github.com/users/vespian/following{/other_user}","gists_url":"https://api.github.com/users/vespian/gists{/gist_id}","starred_url":"https://api.github.com/users/vespian/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/vespian/subscriptions","organizations_url":"https://api.github.com/users/vespian/orgs","repos_url":"https://api.github.com/users/vespian/repos","events_url":"https://api.github.com/users/vespian/events{/privacy}","received_events_url":"https://api.github.com/users/vespian/received_events","type":"User","site_admin":false},"body":"This PR adds a test harness for the adminrouter. It:\r\n* is a part of DCOS-11644\r\n* it solves DCOS-11918\r\n* together with DCOS-11921it will make DCOS-11917 possible.\r\n* moves forward DCOS-9662\r\n\r\nThere is probably still lots that can be done when it comes to docstrings - feel free to comment and suggest something better where applicable.\r\n\r\nPlease start with reading the documentation from the doc/ directory. More detailed descriptions can be found in the docstrings in the code itself.","created_at":"2016-12-19T00:23:11Z","updated_at":"2017-01-12T13:57:36Z","closed_at":null,"merged_at":null,"merge_commit_sha":"2dee3c1dd8dbda85b57f9bbfb7c469aecd42442f","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/dcos/adminrouter/pulls/34/commits","review_comments_url":"https://api.github.com/repos/dcos/adminrouter/pulls/34/comments","review_comment_url":"https://api.github.com/repos/dcos/adminrouter/pulls/comments{/number}","comments_url":"https://api.github.com/repos/dcos/adminrouter/issues/34/comments","statuses_url":"https://api.github.com/repos/dcos/adminrouter/statuses/01d293d0f49acfbbb3d25141034aef79c0952b18","head":{"label":"vespian:prozlach/DCOS-11918_add_test_harness","ref":"prozlach/DCOS-11918_add_test_harness","sha":"01d293d0f49acfbbb3d25141034aef79c0952b18","user":{"login":"vespian","id":2124609,"avatar_url":"https://avatars.githubusercontent.com/u/2124609?v=3","gravatar_id":"","url":"https://api.github.com/users/vespian","html_url":"https://github.com/vespian","followers_url":"https://api.github.com/users/vespian/followers","following_url":"https://api.github.com/users/vespian/following{/other_user}","gists_url":"https://api.github.com/users/vespian/gists{/gist_id}","starred_url":"https://api.github.com/users/vespian/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/vespian/subscriptions","organizations_url":"https://api.github.com/users/vespian/orgs","repos_url":"https://api.github.com/users/vespian/repos","events_url":"https://api.github.com/users/vespian/events{/privacy}","received_events_url":"https://api.github.com/users/vespian/received_events","type":"User","site_admin":false},"repo":{"id":72555673,"name":"adminrouter","full_name":"vespian/adminrouter","owner":{"login":"vespian","id":2124609,"avatar_url":"https://avatars.githubusercontent.com/u/2124609?v=3","gravatar_id":"","url":"https://api.github.com/users/vespian","html_url":"https://github.com/vespian","followers_url":"https://api.github.com/users/vespian/followers","following_url":"https://api.github.com/users/vespian/following{/other_user}","gists_url":"https://api.github.com/users/vespian/gists{/gist_id}","starred_url":"https://api.github.com/users/vespian/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/vespian/subscriptions","organizations_url":"https://api.github.com/users/vespian/orgs","repos_url":"https://api.github.com/users/vespian/repos","events_url":"https://api.github.com/users/vespian/events{/privacy}","received_events_url":"https://api.github.com/users/vespian/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/vespian/adminrouter","description":"Nginx config running on the DCOS master servers to provide a proxy for the admin parts of the cluster","fork":true,"url":"https://api.github.com/repos/vespian/adminrouter","forks_url":"https://api.github.com/repos/vespian/adminrouter/forks","keys_url":"https://api.github.com/repos/vespian/adminrouter/keys{/key_id}","collaborators_url":"https://api.github.com/repos/vespian/adminrouter/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/vespian/adminrouter/teams","hooks_url":"https://api.github.com/repos/vespian/adminrouter/hooks","issue_events_url":"https://api.github.com/repos/vespian/adminrouter/issues/events{/number}","events_url":"https://api.github.com/repos/vespian/adminrouter/events","assignees_url":"https://api.github.com/repos/vespian/adminrouter/assignees{/user}","branches_url":"https://api.github.com/repos/vespian/adminrouter/branches{/branch}","tags_url":"https://api.github.com/repos/vespian/adminrouter/tags","blobs_url":"https://api.github.com/repos/vespian/adminrouter/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/vespian/adminrouter/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/vespian/adminrouter/git/refs{/sha}","trees_url":"https://api.github.com/repos/vespian/adminrouter/git/trees{/sha}","statuses_url":"https://api.github.com/repos/vespian/adminrouter/statuses/{sha}","languages_url":"https://api.github.com/repos/vespian/adminrouter/languages","stargazers_url":"https://api.github.com/repos/vespian/adminrouter/stargazers","contributors_url":"https://api.github.com/repos/vespian/adminrouter/contributors","subscribers_url":"https://api.github.com/repos/vespian/adminrouter/subscribers","subscription_url":"https://api.github.com/repos/vespian/adminrouter/subscription","commits_url":"https://api.github.com/repos/vespian/adminrouter/commits{/sha}","git_commits_url":"https://api.github.com/repos/vespian/adminrouter/git/commits{/sha}","comments_url":"https://api.github.com/repos/vespian/adminrouter/comments{/number}","issue_comment_url":"https://api.github.com/repos/vespian/adminrouter/issues/comments{/number}","contents_url":"https://api.github.com/repos/vespian/adminrouter/contents/{+path}","compare_url":"https://api.github.com/repos/vespian/adminrouter/compare/{base}...{head}","merges_url":"https://api.github.com/repos/vespian/adminrouter/merges","archive_url":"https://api.github.com/repos/vespian/adminrouter/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/vespian/adminrouter/downloads","issues_url":"https://api.github.com/repos/vespian/adminrouter/issues{/number}","pulls_url":"https://api.github.com/repos/vespian/adminrouter/pulls{/number}","milestones_url":"https://api.github.com/repos/vespian/adminrouter/milestones{/number}","notifications_url":"https://api.github.com/repos/vespian/adminrouter/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/vespian/adminrouter/labels{/name}","releases_url":"https://api.github.com/repos/vespian/adminrouter/releases{/id}","deployments_url":"https://api.github.com/repos/vespian/adminrouter/deployments","created_at":"2016-11-01T16:39:04Z","updated_at":"2016-11-01T16:39:05Z","pushed_at":"2017-01-12T12:54:49Z","git_url":"git://github.com/vespian/adminrouter.git","ssh_url":"git@github.com:vespian/adminrouter.git","clone_url":"https://github.com/vespian/adminrouter.git","svn_url":"https://github.com/vespian/adminrouter","homepage":null,"size":404,"stargazers_count":0,"watchers_count":0,"language":"Lua","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"dcos:master","ref":"master","sha":"3c13a05af7ca9225a6c21852ddd161a429aef290","user":{"login":"dcos","id":17648048,"avatar_url":"https://avatars.githubusercontent.com/u/17648048?v=3","gravatar_id":"","url":"https://api.github.com/users/dcos","html_url":"https://github.com/dcos","followers_url":"https://api.github.com/users/dcos/followers","following_url":"https://api.github.com/users/dcos/following{/other_user}","gists_url":"https://api.github.com/users/dcos/gists{/gist_id}","starred_url":"https://api.github.com/users/dcos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dcos/subscriptions","organizations_url":"https://api.github.com/users/dcos/orgs","repos_url":"https://api.github.com/users/dcos/repos","events_url":"https://api.github.com/users/dcos/events{/privacy}","received_events_url":"https://api.github.com/users/dcos/received_events","type":"Organization","site_admin":false},"repo":{"id":56534155,"name":"adminrouter","full_name":"dcos/adminrouter","owner":{"login":"dcos","id":17648048,"avatar_url":"https://avatars.githubusercontent.com/u/17648048?v=3","gravatar_id":"","url":"https://api.github.com/users/dcos","html_url":"https://github.com/dcos","followers_url":"https://api.github.com/users/dcos/followers","following_url":"https://api.github.com/users/dcos/following{/other_user}","gists_url":"https://api.github.com/users/dcos/gists{/gist_id}","starred_url":"https://api.github.com/users/dcos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dcos/subscriptions","organizations_url":"https://api.github.com/users/dcos/orgs","repos_url":"https://api.github.com/users/dcos/repos","events_url":"https://api.github.com/users/dcos/events{/privacy}","received_events_url":"https://api.github.com/users/dcos/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/dcos/adminrouter","description":"Nginx config running on the DCOS master servers to provide a proxy for the admin parts of the cluster","fork":false,"url":"https://api.github.com/repos/dcos/adminrouter","forks_url":"https://api.github.com/repos/dcos/adminrouter/forks","keys_url":"https://api.github.com/repos/dcos/adminrouter/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dcos/adminrouter/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dcos/adminrouter/teams","hooks_url":"https://api.github.com/repos/dcos/adminrouter/hooks","issue_events_url":"https://api.github.com/repos/dcos/adminrouter/issues/events{/number}","events_url":"https://api.github.com/repos/dcos/adminrouter/events","assignees_url":"https://api.github.com/repos/dcos/adminrouter/assignees{/user}","branches_url":"https://api.github.com/repos/dcos/adminrouter/branches{/branch}","tags_url":"https://api.github.com/repos/dcos/adminrouter/tags","blobs_url":"https://api.github.com/repos/dcos/adminrouter/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dcos/adminrouter/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dcos/adminrouter/git/refs{/sha}","trees_url":"https://api.github.com/repos/dcos/adminrouter/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dcos/adminrouter/statuses/{sha}","languages_url":"https://api.github.com/repos/dcos/adminrouter/languages","stargazers_url":"https://api.github.com/repos/dcos/adminrouter/stargazers","contributors_url":"https://api.github.com/repos/dcos/adminrouter/contributors","subscribers_url":"https://api.github.com/repos/dcos/adminrouter/subscribers","subscription_url":"https://api.github.com/repos/dcos/adminrouter/subscription","commits_url":"https://api.github.com/repos/dcos/adminrouter/commits{/sha}","git_commits_url":"https://api.github.com/repos/dcos/adminrouter/git/commits{/sha}","comments_url":"https://api.github.com/repos/dcos/adminrouter/comments{/number}","issue_comment_url":"https://api.github.com/repos/dcos/adminrouter/issues/comments{/number}","contents_url":"https://api.github.com/repos/dcos/adminrouter/contents/{+path}","compare_url":"https://api.github.com/repos/dcos/adminrouter/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dcos/adminrouter/merges","archive_url":"https://api.github.com/repos/dcos/adminrouter/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dcos/adminrouter/downloads","issues_url":"https://api.github.com/repos/dcos/adminrouter/issues{/number}","pulls_url":"https://api.github.com/repos/dcos/adminrouter/pulls{/number}","milestones_url":"https://api.github.com/repos/dcos/adminrouter/milestones{/number}","notifications_url":"https://api.github.com/repos/dcos/adminrouter/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dcos/adminrouter/labels{/name}","releases_url":"https://api.github.com/repos/dcos/adminrouter/releases{/id}","deployments_url":"https://api.github.com/repos/dcos/adminrouter/deployments","created_at":"2016-04-18T19:03:02Z","updated_at":"2016-12-19T17:23:28Z","pushed_at":"2017-01-12T12:54:51Z","git_url":"git://github.com/dcos/adminrouter.git","ssh_url":"git@github.com:dcos/adminrouter.git","clone_url":"https://github.com/dcos/adminrouter.git","svn_url":"https://github.com/dcos/adminrouter","homepage":null,"size":258,"stargazers_count":34,"watchers_count":34,"language":"Lua","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":26,"mirror_url":null,"open_issues_count":2,"forks":26,"open_issues":2,"watchers":34,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/dcos/adminrouter/pulls/34"},"html":{"href":"https://github.com/dcos/adminrouter/pull/34"},"issue":{"href":"https://api.github.com/repos/dcos/adminrouter/issues/34"},"comments":{"href":"https://api.github.com/repos/dcos/adminrouter/issues/34/comments"},"review_comments":{"href":"https://api.github.com/repos/dcos/adminrouter/pulls/34/comments"},"review_comment":{"href":"https://api.github.com/repos/dcos/adminrouter/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/dcos/adminrouter/pulls/34/commits"},"statuses":{"href":"https://api.github.com/repos/dcos/adminrouter/statuses/01d293d0f49acfbbb3d25141034aef79c0952b18"}}}}
{"actor":{"display_login":"gpaul"}}