mirror of
https://git.burble.com/burble.dn42/dn42regsrv.git
synced 2024-02-26 20:28:04 +01:00
Initial Commit
This commit is contained in:
commit
66ca94dccc
83
API.md
Normal file
83
API.md
Normal file
@ -0,0 +1,83 @@
|
||||
# dn42regsrv API Description
|
||||
|
||||
## GET /<file>
|
||||
|
||||
If the StaticRoot configuration option points to a readable directory, files from
|
||||
the directory will be served under /
|
||||
|
||||
The git repository contains a sample StaticRoot directory with a simple registry
|
||||
explorer web app.
|
||||
|
||||
## GET /api/registry/
|
||||
|
||||
Returns a JSON object, with keys for each registry type and values containing a count
|
||||
of the number of registry objects for each type.
|
||||
|
||||
Example:
|
||||
```
|
||||
http://localhost:8042/api/registry/
|
||||
|
||||
# sample output
|
||||
{"as-block":8,"as-set":34,"aut-num":1482,"domain":451,"inet6num":744,"inetnum":1270,"key-cert":7,"mntner":1378,"organisation":275,"person":1387,"registry":4,"role":14,"route":886,"route-set":2,"route6":594,"schema":18,"tinc-key":25,"tinc-keyset":3}
|
||||
```
|
||||
|
||||
|
||||
## GET /api/registry/<type>?match
|
||||
|
||||
Returns a JSON object listing all objects for the matched types.
|
||||
|
||||
Keys for the returned object are registry types, the value for each type is an
|
||||
array of object names
|
||||
|
||||
If the match parameter is provided, the <type> is substring matched against
|
||||
all registry types, otherwise an exact type name is required.
|
||||
|
||||
A special type of '*' returns all types and objects in the registry.
|
||||
|
||||
Example:
|
||||
```
|
||||
http://localhost:8042/api/registry/aut-num # list aut-num objects
|
||||
http://localhost:8042/api/registry/* # list all types and objects
|
||||
http://localhost:8042/api/registry/route?match # list route and route6 objects
|
||||
|
||||
# sample output
|
||||
{"role":["ALENAN-DN42","FLHB-ABUSE-DN42","ORG-SHACK-ADMIN-DN42","PACKETPUSHERS-DN42","CCCHB-ABUSE-DN42","ORG-NETRAVNEN-DN42","ORG-SHACK-ABUSE-DN42","MAGLAB-DN42","NIXNODES-DN42","SOURIS-DN42","CCCKC-DN42","NL-ZUID-DN42","ORG-SHACK-TECH-DN42","ORG-YANE-DN42"]}
|
||||
|
||||
```
|
||||
|
||||
## GET /api/registry/<type>/<object>?match&raw
|
||||
|
||||
Return a JSON object with the registry data for each matching object.
|
||||
|
||||
The keys for the object are the object paths in the form <type>/<object name>. The values depends on the raw parameter.
|
||||
|
||||
if the raw parameter is provided, the returned object consists of a single key 'Attributes'
|
||||
which will be an array of key/value pairs exactly as held within the registry.
|
||||
|
||||
If the raw parameter is not provided, the returned Attributes are decorated with markdown
|
||||
style links depending the relations defined in the DN42 schema. In addition a
|
||||
'Backlinks' key is added which provides an array of registry objects that
|
||||
reference this one.
|
||||
|
||||
If the match parameter is provided, the <object> is substring matched against all
|
||||
objects in the <type>. Matching is case insensitive.
|
||||
|
||||
If the match parameter is not provided, an exact, case sensitive object name is required.
|
||||
|
||||
A special object of '*' returns all objects in the type
|
||||
|
||||
Example:
|
||||
```
|
||||
http://localhost:8042/api/registry/domain/burble.dn42?raw # return object in raw format
|
||||
http://localhost:8042/api/registry/mntner/BURBLE-MNT # return object in decorated format
|
||||
http://localhost:8042/api/registry/aut-num/2601?match # return all aut-num objects matching 2601
|
||||
http://localhost:8042/api/registry/schema/* # return all schema objects
|
||||
|
||||
# sample output (raw)
|
||||
{"domain/burble.dn42":[["domain","burble.dn42"],["descr","burble.dn42 https://dn42.burble.com/"],["admin-c","BURBLE-DN42"],["tech-c","BURBLE-DN42"],["mnt-by","BURBLE-MNT"],["nserver","ns1.burble.dn42 172.20.129.161"],["nserver","ns1.burble.dn42 fd42:4242:2601:ac53::1"],["ds-rdata","61857 13 2 bd35e3efe3325d2029fb652e01604a48b677cc2f44226eeabee54b456c67680c"],["source","DN42"]]}
|
||||
|
||||
# sample output (decorated)
|
||||
{"mntner/BURBLE-MNT":{"Attributes":[["mntner","BURBLE-MNT"],["descr","burble.dn42 https://dn42.burble.com/"],["admin-c","[BURBLE-DN42](person/BURBLE-DN42)"],["tech-c","[BURBLE-DN42](person/BURBLE-DN42)"],["auth","pgp-fingerprint 1C08F282095CCDA432AECC657B9FE8780CFB6593"],["mnt-by","[BURBLE-MNT](mntner/BURBLE-MNT)"],["source","[DN42](registry/DN42)"]],"Backlinks":["as-set/AS4242422601:AS-DOWNSTREAM","as-set/AS4242422601:AS-TRANSIT","inetnum/172.20.129.160_27","person/BURBLE-DN42","route/172.20.129.160_27","inet6num/fd42:4242:2601::_48","mntner/BURBLE-MNT","aut-num/AS4242422601","aut-num/AS4242422602","route6/fd42:4242:2601::_48","domain/collector.dn42","domain/burble.dn42"]}}
|
||||
|
||||
```
|
||||
|
66
README.md
Normal file
66
README.md
Normal file
@ -0,0 +1,66 @@
|
||||
# dn42regsrv
|
||||
|
||||
A REST API for the DN42 registry, written in Go, to provide a bridge between
|
||||
interactive applications and the DN42 registry.
|
||||
|
||||
## Features
|
||||
|
||||
- REST API for querying DN42 registry objects
|
||||
- Able to decorate objects with relationship information based on SCHEMA type definitions
|
||||
- Includes a simple webserver for delivering static files which can be used to deliver
|
||||
basic web applications utilising the API (such as the included DN42 Registry Explorer)
|
||||
- Automatic pull from the DN42 git repository to keep the registry up to date
|
||||
- Included responsive web app for exploring the registry
|
||||
|
||||
## Building
|
||||
|
||||
Requires [git](https://git-scm.com/) and [go](https://golang.org)
|
||||
|
||||
```
|
||||
go get https://git.dn42.us/burble/dn42regsrv
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
Use --help to view configurable options
|
||||
```
|
||||
./dn42regsrv --help
|
||||
```
|
||||
|
||||
The server requires access to a clone of the DN42 registry and for the git executable
|
||||
to be accessible.
|
||||
If you want to use the auto pull feature then the registry must
|
||||
also be writable by the server.
|
||||
|
||||
```
|
||||
cd ${GOROOT}/src/dn42regsrv
|
||||
git clone http://git.dn42.us/dn42/registry.git
|
||||
./dn42regsrv --help
|
||||
./dn42regsrv
|
||||
```
|
||||
|
||||
A sample service file is included for running the server under systemd
|
||||
|
||||
## Using
|
||||
|
||||
By default the server will be listening on port 8042.
|
||||
See the [API.md](API.md) file for a detailed description of the API.
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
Please feel free to raise issues or create pull requests for the project git repository.
|
||||
|
||||
## #ToDo
|
||||
|
||||
### Server
|
||||
|
||||
- Add WHOIS interface
|
||||
- Add endpoints for ROA data
|
||||
- Add attribute searches
|
||||
|
||||
### DN42 Registry Explorer Web App
|
||||
|
||||
- Add search history and fix going back
|
||||
- Allow for attribute searches
|
||||
|
1
StaticRoot/anchorme.min.js
vendored
Normal file
1
StaticRoot/anchorme.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
12
StaticRoot/bootstrap.min.css
vendored
Normal file
12
StaticRoot/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
StaticRoot/dn42_logo.png
Normal file
BIN
StaticRoot/dn42_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
245
StaticRoot/explorer.js
Normal file
245
StaticRoot/explorer.js
Normal file
@ -0,0 +1,245 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Registry Explorer
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// registry-stats component
|
||||
|
||||
Vue.component('registry-stats', {
|
||||
template: '#registry-stats-template',
|
||||
data() {
|
||||
return {
|
||||
state: "loading",
|
||||
error: "",
|
||||
types: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateSearch: function(str) {
|
||||
vm.updateSearch(str)
|
||||
},
|
||||
|
||||
reload: function(event) {
|
||||
this.types = null,
|
||||
this.state = "loading"
|
||||
|
||||
axios
|
||||
.get('/api/registry/')
|
||||
.then(response => {
|
||||
this.types = response.data
|
||||
this.state = 'complete'
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error
|
||||
this.state = 'error'
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.reload()
|
||||
}
|
||||
})
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// registry object component
|
||||
|
||||
Vue.component('reg-object', {
|
||||
template: '#reg-object-template',
|
||||
props: [ 'link' ],
|
||||
data() {
|
||||
return { }
|
||||
},
|
||||
methods: {
|
||||
updateSearch: function(str) {
|
||||
vm.updateSearch(str)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rtype: function() {
|
||||
var ix = this.link.indexOf("/")
|
||||
return this.link.substring(0, ix)
|
||||
},
|
||||
obj: function() {
|
||||
var ix = this.link.indexOf("/")
|
||||
return this.link.substring(ix + 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// reg-attribute component
|
||||
|
||||
Vue.component('reg-attribute', {
|
||||
template: '#reg-attribute-template',
|
||||
props: [ 'content' ],
|
||||
data() {
|
||||
return { }
|
||||
},
|
||||
methods: {
|
||||
isRegObject: function(str) {
|
||||
return (this.content.match(/^\[.*?\]\(.*?\)/) != null)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
objectLink: function() {
|
||||
reg = this.content.match(/^\[(.*?)\]\((.*?)\)/)
|
||||
return reg[2]
|
||||
},
|
||||
decorated: function() {
|
||||
return anchorme(this.content, {
|
||||
truncate: 40,
|
||||
ips: false,
|
||||
attributes: [ { name: "target", value: "_blank" } ]
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// construct a search URL from a search term
|
||||
|
||||
function matchObjects(objects, rtype, term) {
|
||||
|
||||
var results = [ ]
|
||||
|
||||
for (const obj in objects) {
|
||||
var s = objects[obj].toLowerCase()
|
||||
var pos = s.indexOf(term)
|
||||
if (pos != -1) {
|
||||
if ((pos == 0) && (s == term)) {
|
||||
// exact match, return just this result
|
||||
return [[ rtype, objects[obj] ]]
|
||||
}
|
||||
results.push([ rtype, objects[obj] ])
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
function searchFilter(index, term) {
|
||||
|
||||
var results = [ ]
|
||||
|
||||
// comparisons are lowercase
|
||||
term = term.toLowerCase()
|
||||
|
||||
// includes a '/' ? search only in that type
|
||||
var slash = term.indexOf('/')
|
||||
if (slash != -1) {
|
||||
var rtype = term.substring(0, slash)
|
||||
var term = term.substring(slash + 1)
|
||||
objects = index[rtype]
|
||||
if (objects != null) {
|
||||
results = matchObjects(objects, rtype, term)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// walk though the entire index
|
||||
for (const rtype in index) {
|
||||
results = results.concat(matchObjects(index[rtype], rtype, term))
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// main application
|
||||
|
||||
// application data
|
||||
var appData = {
|
||||
searchInput: '',
|
||||
searchTimeout: 0,
|
||||
state: '',
|
||||
debug: "",
|
||||
index: null,
|
||||
filtered: null,
|
||||
result: null
|
||||
}
|
||||
|
||||
// methods
|
||||
var appMethods = {
|
||||
|
||||
loadIndex: function(event) {
|
||||
axios
|
||||
.get('/api/registry/*')
|
||||
.then(response => {
|
||||
this.index = response.data
|
||||
})
|
||||
.catch(error => {
|
||||
// what to do here ?
|
||||
console.log(error)
|
||||
})
|
||||
},
|
||||
|
||||
// called on every search input change
|
||||
debounceSearchInput: function(value) {
|
||||
|
||||
if (this.search_timeout) {
|
||||
clearTimeout(this.search_timeout)
|
||||
}
|
||||
|
||||
// reset if searchbox is empty
|
||||
if (value == "") {
|
||||
this.state = ""
|
||||
this.searchInput = ""
|
||||
this.filtered = null
|
||||
this.results = null
|
||||
return
|
||||
}
|
||||
|
||||
this.search_timeout =
|
||||
setTimeout(this.updateSearch.bind(this,value),500)
|
||||
},
|
||||
|
||||
// called after the search input has been debounced
|
||||
updateSearch: function(value) {
|
||||
this.searchInput = value
|
||||
this.filtered = searchFilter(this.index, value)
|
||||
if (this.filtered.length == 0) {
|
||||
this.state = "noresults"
|
||||
}
|
||||
else if (this.filtered.length == 1) {
|
||||
this.state = "loading"
|
||||
var details = this.filtered[0]
|
||||
|
||||
query = '/api/registry/' + details[0] + '/' + details[1]
|
||||
|
||||
axios
|
||||
.get(query)
|
||||
.then(response => {
|
||||
this.state = 'result'
|
||||
this.result = response.data
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error
|
||||
this.state = 'error'
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.state = "resultlist"
|
||||
this.result = this.filtered
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// intialise Vue instance
|
||||
|
||||
var vm = new Vue({
|
||||
el: '#explorer',
|
||||
data: appData,
|
||||
methods: appMethods,
|
||||
mounted() {
|
||||
this.loadIndex()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
184
StaticRoot/index.html
Normal file
184
StaticRoot/index.html
Normal file
@ -0,0 +1,184 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<style>
|
||||
.material-icons { display:inline-flex;vertical-align:middle }
|
||||
.table th, .table td { padding: 0.2rem; border: none }
|
||||
pre { margin-bottom: 0px }
|
||||
.regref span {
|
||||
padding: 0.3em 1em 0.3em 1em; margin: 0.4em;
|
||||
white-space: nowrap; display:inline-block;
|
||||
}
|
||||
a { cursor: pointer }
|
||||
</style>
|
||||
<title>DN42 Registry Explorer</title>
|
||||
</head>
|
||||
<body style="box-shadow: inset 0 2em 10em rgba(0,0,0,0.4); min-height: 100vh">
|
||||
<div id="explorer">
|
||||
<nav class="navbar navbar-fixed-top navbar-expand-md navbar-dark bg-dark">
|
||||
<form class="form-inline" id="SearchForm">
|
||||
<input v-bind:value="searchInput"
|
||||
v-on:input="debounceSearchInput($event.target.value)"
|
||||
class="form-control-lg" size="30" type="search"
|
||||
placeholder="Search the registry" aria-label="Search"/>
|
||||
</form>
|
||||
<div class="collapse navbar-collapse w-100 ml-auto">
|
||||
<div class="ml-auto"><a class="navbar-brand"
|
||||
href="/">Registry Explorer</a> <a class="pull-right navbar-brand"
|
||||
href="https://dn42.us/"><img src="/dn42_logo.png" width="173"
|
||||
height="60"/></a></div>
|
||||
</nav>
|
||||
<div style="padding: 1em">
|
||||
|
||||
<div v-show="searchInput == ''">
|
||||
|
||||
<div class="jumbotron">
|
||||
<h1>DN42 Registry Explorer</h1>
|
||||
<p class="lead">Just start typing in the search box to start searching the registry</p>
|
||||
<hr/>
|
||||
<p>
|
||||
<p>Search Tips</p>
|
||||
<ul>
|
||||
<li>Searches are case independent
|
||||
<li>No need to hit enter, searches will start immediately
|
||||
<li>Prefixing the search by a registry type followed by / will narrow the search to
|
||||
just that type (e.g. <a v-on:click="updateSearch('domain/.dn42')"
|
||||
class="text-success">domain/.dn42</a> )
|
||||
<li>Searching for <b>type/</b> will return all the objects for that type (e.g.
|
||||
<a v-on:click="updateSearch('schema/')" class="text-success">schema/</a> )
|
||||
<li>A blank search box will return you to these instructions
|
||||
<li>Searches are made on object names; searching the content of objects
|
||||
is not supported (yet!).
|
||||
<li>Going back (or any search history) is also not supported yet.
|
||||
</ul>
|
||||
<hr/>
|
||||
<p>The registry explorer is a simple web app using
|
||||
<a href="https://git.dn42.us/burble/dn42regsrv">dn42regsrv</a>;
|
||||
a REST API for the DN42 registry built with <a href="https://golang.org/">Go</a>
|
||||
</div>
|
||||
|
||||
<registry-stats/>
|
||||
|
||||
</div>
|
||||
|
||||
<section v-if="state == 'loading'">
|
||||
<div class="alert alert-info" role="alert">
|
||||
Loading data ...
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="state == 'error'">
|
||||
<div class="alert alert-primary clearfix" role="alert">
|
||||
An error recurred whilst retrieving data <button type="button" class="float-right btn btn-warning" v-on:click="reload"><span class="material-icons">refresh</span> Refresh</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="state == 'noresults'">
|
||||
<h2>Searching for "{{ searchInput }}" ...</h2>
|
||||
<div class="alert alert-dark" role="alert">
|
||||
Sorry, no results found
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="state == 'resultlist'">
|
||||
<h2>Listing results for "{{ searchInput }}" ...</h2>
|
||||
<div class="container d-flex flex-row flex-wrap">
|
||||
<div style="text-align: center">
|
||||
<span v-for="value in result" style="margin: 0.5em 1em 0.5em 1em; display:inline-block">
|
||||
<reg-object v-bind:link="value[0] + '/' + value[1]"></reg-object>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="state == 'result'">
|
||||
<div v-for="(val, key) in result">
|
||||
<h2><reg-object v-bind:link="key"></reg-object></h2>
|
||||
<div style="padding-left: 2em">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th scope="col">Key</th><th scope="col">Value</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="a in val.Attributes">
|
||||
<th scope="row" class="text-primary" style="white-space:nowrap">{{ a[0] }}</th>
|
||||
<td><reg-attribute v-bind:content="a[1]"></reg-attribute></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<section v-if="val.Backlinks.length != 0">
|
||||
<p>Referenced by</p>
|
||||
<div style="padding-left: 2em">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr v-for="r in val.Backlinks">
|
||||
<td><reg-object v-bind:link="r"></reg-object></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<footer class="page-footer font-small">
|
||||
<div style="margin-top: 20px; padding: 5px">
|
||||
<a href="https://git.dn42.us/burble/dn42regsrv">Source Code</a>.
|
||||
Powered by
|
||||
<a href="https://getbootstrap.com/">Bootstrap</a>,
|
||||
<a href="https://vuejs.org">Vue.js</a>,
|
||||
<a href="https://github.com/axios/axios">axios</a>,
|
||||
<a href="http://alexcorvi.github.io/anchorme.js/">Anchorme</a>.
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script type="text/x-template" id="registry-stats-template">
|
||||
<div class="container d-flex flex-column w-75">
|
||||
<h5>Registry Stats</h5>
|
||||
<section v-if="state == 'error'">
|
||||
<div class="alert alert-primary clearfix" role="alert">
|
||||
An error recurred whilst retrieving data <button type="button" class="float-right btn btn-warning" v-on:click="reload"><span class="material-icons">refresh</span> Refresh</button>
|
||||
</div>
|
||||
</section>
|
||||
<section v-else-if="state == 'loading'">
|
||||
<div class="alert alert-info" role="alert">
|
||||
Loading data ...
|
||||
</div>
|
||||
</section>
|
||||
<section v-else>
|
||||
<p style="padding:1em;text-align:center">
|
||||
<span v-for="(value, key) in types" style="margin-left:0.5em;margin-right:0.5em;white-space:nowrap;display:inline-block"><a v-on:click="updateSearch(key + '/')" class="text-success">{{ key }}</a>: <b>{{ value }}</b> records</span>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="reg-object-template">
|
||||
<span class="regref"><a v-on:click="updateSearch(rtype + '/' + obj)" class="text-success"
|
||||
style="margin-right: 0.4em">{{ obj }}</a> <span
|
||||
class="badge badge-pill badge-dark text-muted">{{ rtype }}</span></span></span>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="reg-attribute-template">
|
||||
<span style="word-break: break-all">
|
||||
<reg-object v-if="isRegObject()" v-bind:link="objectLink"></reg-object>
|
||||
<span v-else class="text-monospace" v-html="decorated"></span>
|
||||
</span>
|
||||
</script>
|
||||
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.2/dist/vue.js"></script>
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/vue@2.6.2/dist/vue.min.js"></script> -->
|
||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||
<script src="anchorme.min.js"></script>
|
||||
<script src="explorer.js"></script>
|
||||
</body>
|
||||
</html>
|
169
dn42regsrv.go
Normal file
169
dn42regsrv.go
Normal file
@ -0,0 +1,169 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Registry API Server
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
flag "github.com/spf13/pflag"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// list of API endpoints
|
||||
|
||||
type InitEndpointFunc = func(route *mux.Router)
|
||||
|
||||
var apiEndpoints = make([]InitEndpointFunc, 0)
|
||||
|
||||
func RegisterAPIEndpoint(f InitEndpointFunc) {
|
||||
apiEndpoints = append(apiEndpoints, f)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// utility function to set the log level
|
||||
|
||||
func setLogLevel(levelStr string) {
|
||||
|
||||
if level, err := log.ParseLevel(levelStr); err != nil {
|
||||
// failed to set the level
|
||||
|
||||
// set a sensible default and, of course, log the error
|
||||
log.SetLevel(log.InfoLevel)
|
||||
log.WithFields(log.Fields{
|
||||
"loglevel": levelStr,
|
||||
"error": err,
|
||||
}).Error("Failed to set requested log level")
|
||||
|
||||
} else {
|
||||
|
||||
// set the requested level
|
||||
log.SetLevel(level)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// http request logger
|
||||
|
||||
func requestLogger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"method": r.Method,
|
||||
"URL": r.URL.String(),
|
||||
"Remote": r.RemoteAddr,
|
||||
}).Debug("HTTP Request")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// everything starts here
|
||||
|
||||
func main() {
|
||||
|
||||
// set a default log level, so that logging can be used immediately
|
||||
// the level will be overidden later on once the command line
|
||||
// options are loaded
|
||||
log.SetLevel(log.InfoLevel)
|
||||
log.Info("DN42 Registry API Server Starting")
|
||||
|
||||
// declare cmd line options
|
||||
var (
|
||||
logLevel = flag.StringP("LogLevel", "l", "Info", "Log level")
|
||||
regDir = flag.StringP("RegDir", "d", "registry", "Registry data directory")
|
||||
bindAddress = flag.StringP("BindAddress", "b", "[::]:8042", "Server bind address")
|
||||
staticRoot = flag.StringP("StaticRoot", "s", "StaticRoot", "Static page directory")
|
||||
refreshInterval = flag.StringP("Refresh", "i", "60m", "Refresh interval")
|
||||
gitPath = flag.StringP("GitPath", "g", "/usr/bin/git", "Path to git executable")
|
||||
autoPull = flag.BoolP("AutoPull", "a", true, "Automatically pull the registry")
|
||||
pullURL = flag.StringP("PullURL", "p", "origin", "URL to auto pull")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
// now initialise logging properly based on the cmd line options
|
||||
setLogLevel(*logLevel)
|
||||
|
||||
// parse the refreshInterval and start data collection
|
||||
interval, err := time.ParseDuration(*refreshInterval)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"interval": *refreshInterval,
|
||||
}).Fatal("Unable to parse registry refresh interval")
|
||||
}
|
||||
|
||||
InitialiseRegistryData(*regDir, interval,
|
||||
*gitPath, *autoPull, *pullURL)
|
||||
|
||||
// initialise router
|
||||
router := mux.NewRouter()
|
||||
// log all access
|
||||
router.Use(requestLogger)
|
||||
|
||||
// initialise API routes
|
||||
subr := router.PathPrefix("/api").Subrouter()
|
||||
for _, epInit := range apiEndpoints {
|
||||
epInit(subr)
|
||||
}
|
||||
|
||||
// initialise static routes
|
||||
InstallStaticRoutes(router, *staticRoot)
|
||||
|
||||
// initialise http server
|
||||
server := &http.Server{
|
||||
Addr: *bindAddress,
|
||||
WriteTimeout: time.Second * 15,
|
||||
ReadTimeout: time.Second * 15,
|
||||
IdleTimeout: time.Second * 60,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
// run the server in a non-blocking goroutine
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"BindAddress": *bindAddress,
|
||||
}).Info("Starting server")
|
||||
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"BindAddress": *bindAddress,
|
||||
}).Fatal("Unable to start server")
|
||||
}
|
||||
}()
|
||||
|
||||
// graceful shutdown via SIGINT (^C)
|
||||
csig := make(chan os.Signal, 1)
|
||||
signal.Notify(csig, os.Interrupt)
|
||||
|
||||
// and block
|
||||
<-csig
|
||||
|
||||
log.Info("Server shutting down")
|
||||
|
||||
// deadline for server to shutdown
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10)
|
||||
defer cancel()
|
||||
|
||||
// shutdown the server
|
||||
server.Shutdown(ctx)
|
||||
|
||||
// nothing left to do
|
||||
log.Info("Shutdown complete, all done")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
267
regapi.go
Normal file
267
regapi.go
Normal file
@ -0,0 +1,267 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Registry API Server
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"strings"
|
||||
// "time"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// register the api
|
||||
|
||||
func init() {
|
||||
RegisterAPIEndpoint(InitRegAPI)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// called from main to initialise the API routing
|
||||
|
||||
func InitRegAPI(router *mux.Router) {
|
||||
|
||||
s := router.
|
||||
Methods("GET").
|
||||
PathPrefix("/registry").
|
||||
Subrouter()
|
||||
|
||||
s.HandleFunc("/", regRootHandler)
|
||||
//s.HandleFunc("/.schema", rTypeListHandler)
|
||||
//s.HandleFunc("/.meta/", rTypeListHandler)
|
||||
|
||||
s.HandleFunc("/{type}", regTypeHandler)
|
||||
s.HandleFunc("/{type}/{object}", regObjectHandler)
|
||||
|
||||
log.Info("Registry API installed")
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// handler utility funcs
|
||||
|
||||
func responseJSON(w http.ResponseWriter, v interface{}) {
|
||||
|
||||
// for response time testing
|
||||
//time.Sleep(time.Second)
|
||||
|
||||
// marshal the JSON string
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Error("Failed to marshal JSON")
|
||||
}
|
||||
|
||||
// write back to http handler
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// root handler, lists all types within the registry
|
||||
|
||||
func regRootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
response := make(map[string]int)
|
||||
for _, rType := range RegistryData.Types {
|
||||
response[rType.Ref] = len(rType.Objects)
|
||||
}
|
||||
responseJSON(w, response)
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// type handler returns list of objects that match the type
|
||||
|
||||
func regTypeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// request parameters
|
||||
vars := mux.Vars(r)
|
||||
query := r.URL.Query()
|
||||
|
||||
typeName := vars["type"] // type name to list
|
||||
match := query["match"] // single query or match
|
||||
|
||||
// special case to return all types
|
||||
all := false
|
||||
if typeName == "*" {
|
||||
match = []string{}
|
||||
all = true
|
||||
}
|
||||
|
||||
// results will hold the types to return
|
||||
var results []*RegType
|
||||
|
||||
// check match type
|
||||
if match == nil {
|
||||
// exact match
|
||||
|
||||
// check the type object exists
|
||||
rType := RegistryData.Types[typeName]
|
||||
if rType == nil {
|
||||
http.Error(w, "No types matching '"+typeName+"' found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// return just a single result
|
||||
results = []*RegType{rType}
|
||||
|
||||
} else {
|
||||
// substring match
|
||||
|
||||
// comparisons are lower case
|
||||
typeName = strings.ToLower(typeName)
|
||||
|
||||
// walk through the types and filter to the results list
|
||||
results = make([]*RegType, 0)
|
||||
for key, rType := range RegistryData.Types {
|
||||
if all || strings.Contains(strings.ToLower(key), typeName) {
|
||||
// match found, add to the list
|
||||
results = append(results, rType)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// construct the response
|
||||
response := make(map[string][]string)
|
||||
for _, rType := range results {
|
||||
|
||||
objects := make([]string, 0, len(rType.Objects))
|
||||
for key := range rType.Objects {
|
||||
objects = append(objects, key)
|
||||
}
|
||||
|
||||
response[rType.Ref] = objects
|
||||
}
|
||||
|
||||
responseJSON(w, response)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// object handler returns object data
|
||||
|
||||
// per object response structure
|
||||
type RegObjectResponse struct {
|
||||
Attributes [][2]string
|
||||
Backlinks []string
|
||||
}
|
||||
|
||||
func regObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// request parameters
|
||||
vars := mux.Vars(r)
|
||||
query := r.URL.Query()
|
||||
|
||||
typeName := vars["type"] // object type
|
||||
objName := vars["object"] // object name or match
|
||||
match := query["match"] // single query or match
|
||||
raw := query["raw"] // raw or decorated results
|
||||
|
||||
// special case to return all objects
|
||||
all := false
|
||||
if objName == "*" {
|
||||
match = []string{}
|
||||
all = true
|
||||
}
|
||||
|
||||
// verify the type exists
|
||||
rType := RegistryData.Types[typeName]
|
||||
if rType == nil {
|
||||
http.Error(w, "No types matching '"+typeName+"' found",
|
||||
http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// results will hold the objects to return
|
||||
var results []*RegObject
|
||||
|
||||
// check match type
|
||||
if match == nil {
|
||||
// exact match
|
||||
|
||||
// check the object exists
|
||||
object := rType.Objects[objName]
|
||||
if object == nil {
|
||||
http.Error(w, "No objects matching '"+objName+"' found",
|
||||
http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// then just create a results list with one object
|
||||
results = []*RegObject{object}
|
||||
|
||||
} else {
|
||||
// substring matching
|
||||
|
||||
// comparisons are lower case
|
||||
objName = strings.ToLower(objName)
|
||||
|
||||
// walk through the type objects and filter to the results list
|
||||
results = make([]*RegObject, 0)
|
||||
for key, object := range rType.Objects {
|
||||
if all || strings.Contains(strings.ToLower(key), objName) {
|
||||
// match found, add to the list
|
||||
results = append(results, object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collate the results in to the response data
|
||||
if raw == nil {
|
||||
// provide a decorated response
|
||||
response := make(map[string]RegObjectResponse)
|
||||
|
||||
// for each object in the results
|
||||
for _, object := range results {
|
||||
|
||||
// copy the raw attributes
|
||||
attributes := make([][2]string, len(object.Data))
|
||||
for ix, attribute := range object.Data {
|
||||
attributes[ix] = [2]string{attribute.Key, attribute.Value}
|
||||
}
|
||||
|
||||
// construct the backlinks
|
||||
backlinks := make([]string, len(object.Backlinks))
|
||||
for ix, object := range object.Backlinks {
|
||||
backlinks[ix] = object.Ref
|
||||
}
|
||||
|
||||
// add to the response
|
||||
response[object.Ref] = RegObjectResponse{
|
||||
Attributes: attributes,
|
||||
Backlinks: backlinks,
|
||||
}
|
||||
}
|
||||
|
||||
responseJSON(w, response)
|
||||
|
||||
} else {
|
||||
// provide a response with just the raw registry data
|
||||
response := make(map[string][][2]string)
|
||||
|
||||
// for each object in the results
|
||||
for _, object := range results {
|
||||
|
||||
attributes := make([][2]string, len(object.Data))
|
||||
response[object.Ref] = attributes
|
||||
|
||||
// copy the raw attributes
|
||||
for ix, attribute := range object.Data {
|
||||
attributes[ix] = [2]string{attribute.Key, attribute.RawValue}
|
||||
}
|
||||
}
|
||||
|
||||
responseJSON(w, response)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
638
registry.go
Normal file
638
registry.go
Normal file
@ -0,0 +1,638 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Registry API Server
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
// "errors"
|
||||
"fmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// registry data model
|
||||
|
||||
// registry data
|
||||
|
||||
// Attributes within Objects
|
||||
type RegAttribute struct {
|
||||
Key string
|
||||
Value string // this is a post-processed, or decorated value
|
||||
RawValue string // the raw value as read from the registry
|
||||
}
|
||||
|
||||
type RegObject struct {
|
||||
Ref string // the ref contains the full path for this object
|
||||
Data []*RegAttribute // the key/value data for this object
|
||||
Backlinks []*RegObject // other objects that reference this one
|
||||
}
|
||||
|
||||
// types are collections of objects
|
||||
type RegType struct {
|
||||
Ref string // full path for this type
|
||||
Objects map[string]*RegObject // the objects in this type
|
||||
}
|
||||
|
||||
// registry meta data
|
||||
|
||||
type RegAttributeSchema struct {
|
||||
Fields []string
|
||||
Relations []*RegType
|
||||
}
|
||||
|
||||
type RegTypeSchema struct {
|
||||
Ref string
|
||||
Attributes map[string]*RegAttributeSchema
|
||||
}
|
||||
|
||||
// the registry itself
|
||||
|
||||
type Registry struct {
|
||||
Schema map[string]*RegTypeSchema
|
||||
Types map[string]*RegType
|
||||
}
|
||||
|
||||
// and a variable for the actual data
|
||||
var RegistryData *Registry
|
||||
|
||||
// store the current commit has
|
||||
var previousCommit string
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// utility and manipulation functions
|
||||
|
||||
// general functions
|
||||
|
||||
func RegistryMakePath(t string, o string) string {
|
||||
return t + "/" + o
|
||||
}
|
||||
|
||||
// attribute functions
|
||||
|
||||
// return a pointer to a RegType from a decorated attribute value
|
||||
func (*RegAttribute) ExtractRegType() *RegType {
|
||||
return nil
|
||||
}
|
||||
|
||||
// object functions
|
||||
|
||||
// return attributes exactly matching a specific key
|
||||
func (object *RegObject) GetKey(key string) []*RegAttribute {
|
||||
|
||||
attributes := make([]*RegAttribute, 0)
|
||||
for _, a := range object.Data {
|
||||
if a.Key == key {
|
||||
attributes = append(attributes, a)
|
||||
}
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
// return a single key
|
||||
func (object *RegObject) GetSingleKey(key string) *RegAttribute {
|
||||
|
||||
attributes := object.GetKey(key)
|
||||
if len(attributes) != 1 {
|
||||
log.WithFields(log.Fields{
|
||||
"key": key,
|
||||
"object": object.Ref,
|
||||
}).Error("Unable to find unique key in object")
|
||||
|
||||
// can't register the object
|
||||
return nil
|
||||
}
|
||||
return attributes[0]
|
||||
}
|
||||
|
||||
// schema functions
|
||||
|
||||
// validate a set of attributes against a schema
|
||||
func (schema *RegTypeSchema) validate(attributes []*RegAttribute) []*RegAttribute {
|
||||
|
||||
validated := make([]*RegAttribute, 0, len(attributes))
|
||||
for _, attribute := range attributes {
|
||||
|
||||
// keys beginning with 'x-' are user defined, skip validation
|
||||
if !strings.HasPrefix(attribute.Key, "x-") {
|
||||
if schema.Attributes[attribute.Key] == nil {
|
||||
// couldn't find a schema attribute
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"key": attribute.Key,
|
||||
"schema": schema.Ref,
|
||||
}).Error("Schema validation failed")
|
||||
|
||||
// don't add to the validated list
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// all ok
|
||||
validated = append(validated, attribute)
|
||||
}
|
||||
|
||||
return validated
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// reload the registry
|
||||
|
||||
func reloadRegistry(path string) {
|
||||
|
||||
log.Debug("Reloading registry")
|
||||
|
||||
// r will become the new registry data
|
||||
registry := &Registry{
|
||||
Schema: make(map[string]*RegTypeSchema),
|
||||
Types: make(map[string]*RegType),
|
||||
}
|
||||
|
||||
// bootstrap the schema registry type
|
||||
registry.Types["schema"] = &RegType{
|
||||
Ref: "schema",
|
||||
Objects: make(map[string]*RegObject),
|
||||
}
|
||||
registry.loadType("schema", path)
|
||||
|
||||
// and parse the schema to get the remaining types
|
||||
registry.parseSchema()
|
||||
|
||||
// now load the remaining types
|
||||
for _, rType := range registry.Types {
|
||||
registry.loadType(rType.Ref, path)
|
||||
}
|
||||
|
||||
// mark relationships
|
||||
registry.decorate()
|
||||
|
||||
// swap in the new registry data
|
||||
RegistryData = registry
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// create and load the raw data for a registry type
|
||||
|
||||
func (registry *Registry) loadType(typeName string, path string) {
|
||||
|
||||
// the type will already have been created
|
||||
rType := registry.Types[typeName]
|
||||
|
||||
// as will the schema (unless attempting to load the schema itself)
|
||||
schema := registry.Schema[typeName]
|
||||
|
||||
// special case for DNS as the directory
|
||||
// doesn't match the type name
|
||||
if typeName == "domain" {
|
||||
path += "/dns"
|
||||
} else {
|
||||
path += "/" + typeName
|
||||
}
|
||||
|
||||
// and load all the objects in this type
|
||||
rType.loadObjects(schema, path)
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// load all the objects associated with a type
|
||||
|
||||
func (rType *RegType) loadObjects(schema *RegTypeSchema, path string) {
|
||||
|
||||
entries, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": path,
|
||||
"type": rType.Ref,
|
||||
}).Error("Failed to read registry type directory")
|
||||
return
|
||||
}
|
||||
|
||||
// for each entry in the directory
|
||||
for _, entry := range entries {
|
||||
|
||||
// each file maps to a registry object
|
||||
if !entry.IsDir() {
|
||||
|
||||
filename := entry.Name()
|
||||
// ignore dotfiles
|
||||
if !strings.HasPrefix(filename, ".") {
|
||||
|
||||
// load the attributes from file
|
||||
attributes := loadAttributes(path + "/" + filename)
|
||||
|
||||
// basic validation of attributes against the schema
|
||||
// schema may be nil if we are actually loading the schema itself
|
||||
if schema != nil {
|
||||
attributes = schema.validate(attributes)
|
||||
}
|
||||
|
||||
// make the object
|
||||
object := &RegObject{
|
||||
Ref: RegistryMakePath(rType.Ref, filename),
|
||||
Data: attributes,
|
||||
Backlinks: make([]*RegObject, 0),
|
||||
}
|
||||
|
||||
// add to type
|
||||
rType.Objects[filename] = object
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"ref": rType.Ref,
|
||||
"path": path,
|
||||
"count": len(rType.Objects),
|
||||
}).Debug("Loaded registry type")
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// read attributes from a file
|
||||
|
||||
func loadAttributes(path string) []*RegAttribute {
|
||||
|
||||
attributes := make([]*RegAttribute, 0)
|
||||
|
||||
// open the file to start reading it
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": path,
|
||||
}).Error("Failed to read attributes from file")
|
||||
return attributes
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// read the file line by line using the bufio scanner
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
|
||||
line := strings.TrimRight(scanner.Text(), "\r\n")
|
||||
runes := []rune(line)
|
||||
|
||||
// lines starting with '+' denote an empty line
|
||||
if runes[0] == rune('+') {
|
||||
|
||||
// concatenate a \n on to the previous attribute value
|
||||
attributes[len(attributes)-1].RawValue += "\n"
|
||||
|
||||
} else {
|
||||
|
||||
// look for a : separator in the first 20 characters
|
||||
ix := strings.IndexByte(line, ':')
|
||||
if ix == -1 || ix >= 20 {
|
||||
// couldn't find one
|
||||
|
||||
if len(runes) <= 20 {
|
||||
// hmmm, the line was shorter than 20 characters
|
||||
// something is amiss
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"length": len(runes),
|
||||
"path": path,
|
||||
"line": line,
|
||||
}).Warn("Short line detected")
|
||||
|
||||
} else {
|
||||
|
||||
// line is a continuation of the previous line, so
|
||||
// concatenate the value on to the previous attribute value
|
||||
attributes[len(attributes)-1].RawValue +=
|
||||
"\n" + string(runes[20:])
|
||||
|
||||
}
|
||||
} else {
|
||||
// found a key and : separator
|
||||
|
||||
// is there actually a value ?
|
||||
var value string
|
||||
if len(runes) <= 20 {
|
||||
// blank value
|
||||
value = ""
|
||||
} else {
|
||||
value = string(runes[20:])
|
||||
}
|
||||
|
||||
// create a new attribute
|
||||
a := &RegAttribute{
|
||||
Key: string(runes[:ix]),
|
||||
RawValue: value,
|
||||
}
|
||||
attributes = append(attributes, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// parse schema files to extract keys and for attribute relations
|
||||
|
||||
func (registry *Registry) parseSchema() {
|
||||
|
||||
// for each object in the schema type
|
||||
for _, object := range registry.Types["schema"].Objects {
|
||||
|
||||
// look up the ref attribute
|
||||
ref := object.GetSingleKey("ref")
|
||||
if ref == nil {
|
||||
log.WithFields(log.Fields{
|
||||
"object": object.Ref,
|
||||
}).Error("Schema record without ref")
|
||||
|
||||
// can't process this object
|
||||
continue
|
||||
}
|
||||
|
||||
// create the type schema object
|
||||
typeName := strings.TrimPrefix(ref.RawValue, "dn42.")
|
||||
typeSchema := &RegTypeSchema{
|
||||
Ref: typeName,
|
||||
Attributes: make(map[string]*RegAttributeSchema),
|
||||
}
|
||||
|
||||
// ensure the type exists
|
||||
rType := registry.Types[typeName]
|
||||
if rType == nil {
|
||||
rType := &RegType{
|
||||
Ref: typeName,
|
||||
Objects: make(map[string]*RegObject),
|
||||
}
|
||||
registry.Types[typeName] = rType
|
||||
}
|
||||
|
||||
// for each key attribute in the schema
|
||||
attributes := object.GetKey("key")
|
||||
for _, attribute := range attributes {
|
||||
|
||||
// split the value on whitespace
|
||||
fields := strings.Fields(attribute.RawValue)
|
||||
keyName := fields[0]
|
||||
|
||||
typeSchema.Attributes[keyName] = &RegAttributeSchema{
|
||||
Fields: fields[1:],
|
||||
}
|
||||
}
|
||||
|
||||
// register the type schema
|
||||
registry.Schema[typeName] = typeSchema
|
||||
|
||||
}
|
||||
|
||||
// scan the fields of each schema attribute to determine relationships
|
||||
// this needs to be second step to allow pre-creation of the types
|
||||
for _, typeSchema := range registry.Schema {
|
||||
for attribName, attribSchema := range typeSchema.Attributes {
|
||||
for _, field := range attribSchema.Fields {
|
||||
if strings.HasPrefix(field, "lookup=") {
|
||||
|
||||
// the relationships may be a multivalue, separated by ,
|
||||
rels := strings.Split(strings.
|
||||
TrimPrefix(field, "lookup="), ",")
|
||||
|
||||
// map to a regtype
|
||||
relations := make([]*RegType, 0, len(rels))
|
||||
for ix := range rels {
|
||||
relName := strings.TrimPrefix(rels[ix], "dn42.")
|
||||
relation := registry.Types[relName]
|
||||
|
||||
// log if unable to look up the type
|
||||
if relation == nil {
|
||||
// log unless this is the schema def lookup=str '>' [spec]...
|
||||
if typeSchema.Ref != "schema" {
|
||||
log.WithFields(log.Fields{
|
||||
"relation": relName,
|
||||
"attribute": attribName,
|
||||
"type": typeSchema.Ref,
|
||||
}).Error("Relation to type that does not exist")
|
||||
}
|
||||
|
||||
} else {
|
||||
// store the relationship
|
||||
relations = append(relations, relation)
|
||||
}
|
||||
}
|
||||
|
||||
// register the relations
|
||||
attribSchema.Relations = relations
|
||||
|
||||
// assume only 1 lookup= per key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("Schema parsing complete")
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// parse all attributes and decorate them
|
||||
|
||||
func (registry *Registry) decorate() {
|
||||
|
||||
cattribs := 0
|
||||
cmatched := 0
|
||||
|
||||
// walk each attribute value
|
||||
for _, rType := range registry.Types {
|
||||
schema := registry.Schema[rType.Ref]
|
||||
for _, object := range rType.Objects {
|
||||
for _, attribute := range object.Data {
|
||||
cattribs += 1
|
||||
|
||||
attribSchema := schema.Attributes[attribute.Key]
|
||||
// are there relations defined for this attribute ?
|
||||
// attribSchema may be null if this attribute is user defined (x-*)
|
||||
if (attribSchema != nil) && attribute.matchRelation(object,
|
||||
attribSchema.Relations) {
|
||||
// matched
|
||||
cmatched += 1
|
||||
} else {
|
||||
// no match, just copy the attribute data
|
||||
attribute.Value = attribute.RawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"attributes": cattribs,
|
||||
"matched": cmatched,
|
||||
}).Debug("Decoration complete")
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// match an attribute against schema relations
|
||||
|
||||
func (attribute *RegAttribute) matchRelation(parent *RegObject,
|
||||
relations []*RegType) bool {
|
||||
|
||||
// it's not going to match if relations is empty
|
||||
if relations == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// check each relation
|
||||
for _, relation := range relations {
|
||||
|
||||
object := relation.Objects[attribute.RawValue]
|
||||
if object != nil {
|
||||
// found a match !
|
||||
|
||||
// decorate the attribute value
|
||||
attribute.Value = fmt.Sprintf("[%s](%s)",
|
||||
attribute.RawValue, object.Ref)
|
||||
|
||||
// and add a back reference to the related object
|
||||
object.Backlinks = append(object.Backlinks, parent)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// didn't find anything
|
||||
return false
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// fetch the current commit hash
|
||||
|
||||
func getCommitHash(regDir string, gitPath string) string {
|
||||
|
||||
// run git to get the latest commit hash
|
||||
cmd := exec.Command(gitPath, "log", "-1", "--format=%H")
|
||||
cmd.Dir = regDir
|
||||
// execute
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"gitPath": gitPath,
|
||||
"regDir": regDir,
|
||||
}).Error("Failed to execute git log")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// refresh the registry
|
||||
|
||||
func refreshRegistry(regDir string, gitPath string, pullURL string) {
|
||||
|
||||
// run git to get the latest commit hash
|
||||
cmd := exec.Command(gitPath, "pull", pullURL)
|
||||
cmd.Dir = regDir
|
||||
// execute
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"gitPath": gitPath,
|
||||
"regDir": regDir,
|
||||
"pullURL": pullURL,
|
||||
}).Error("Failed to execute git log")
|
||||
}
|
||||
|
||||
fmt.Println(string(out))
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// called from main to initialse the registry data and syncing
|
||||
|
||||
func InitialiseRegistryData(regDir string, refresh time.Duration,
|
||||
gitPath string, autoPull bool, pullURL string) {
|
||||
|
||||
// validate that the regDir/data path exists
|
||||
dataPath := regDir + "/data"
|
||||
regStat, err := os.Stat(dataPath)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": dataPath,
|
||||
}).Fatal("Unable to find registry directory")
|
||||
}
|
||||
|
||||
// and it is a directory
|
||||
if !regStat.IsDir() {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": dataPath,
|
||||
}).Fatal("Registry path is not a directory")
|
||||
}
|
||||
|
||||
// check that git exists
|
||||
_, err = os.Stat(gitPath)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": gitPath,
|
||||
}).Fatal("Unable to find git executable")
|
||||
}
|
||||
|
||||
// enforce a minimum update time
|
||||
minTime := 10 * time.Minute
|
||||
if refresh < minTime {
|
||||
log.WithFields(log.Fields{
|
||||
"interval": refresh,
|
||||
}).Error("Enforcing minimum update time of 10 minutes")
|
||||
|
||||
refresh = minTime
|
||||
}
|
||||
|
||||
// initialise the previous commit hash
|
||||
// and do initial load from registry
|
||||
previousCommit = getCommitHash(regDir, gitPath)
|
||||
reloadRegistry(dataPath)
|
||||
|
||||
go func() {
|
||||
|
||||
// every refresh interval
|
||||
for range time.Tick(refresh) {
|
||||
log.Debug("Refresh Timer")
|
||||
|
||||
// automatically try to refresh the registry ?
|
||||
if autoPull {
|
||||
refreshRegistry(regDir, gitPath, pullURL)
|
||||
}
|
||||
|
||||
// get the latest hash
|
||||
currentCommit := getCommitHash(regDir, gitPath)
|
||||
|
||||
// has the registry been updated ?
|
||||
if currentCommit != previousCommit {
|
||||
log.WithFields(log.Fields{
|
||||
"current": currentCommit,
|
||||
"previous": previousCommit,
|
||||
}).Info("Registry has changed, refresh started")
|
||||
|
||||
// refresh
|
||||
reloadRegistry(dataPath)
|
||||
|
||||
// update commit
|
||||
previousCommit = currentCommit
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
55
static.go
Normal file
55
static.go
Normal file
@ -0,0 +1,55 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Registry API Server
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// called from main to initialise the API routing
|
||||
|
||||
func InstallStaticRoutes(router *mux.Router, staticPath string) {
|
||||
|
||||
// an empty path disables static route serving
|
||||
if staticPath == "" {
|
||||
log.Info("Disabling static route serving")
|
||||
return
|
||||
}
|
||||
|
||||
// validate that the staticPath exists
|
||||
stat, err := os.Stat(staticPath)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": staticPath,
|
||||
}).Fatal("Unable to find static page directory")
|
||||
}
|
||||
|
||||
// and it is a directory
|
||||
if !stat.IsDir() {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": staticPath,
|
||||
}).Fatal("Static path is not a directory")
|
||||
}
|
||||
|
||||
// install a file server for the static route
|
||||
router.PathPrefix("/").Handler(http.StripPrefix("/",
|
||||
http.FileServer(http.Dir(staticPath)))).Methods("GET")
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"path": staticPath,
|
||||
}).Info("Static route installed")
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
Loading…
Reference in New Issue
Block a user